From 4c0f278229c4d6594187ac09fe9ac8e3b3d23a37 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Sun, 31 May 2026 14:06:48 +0200 Subject: [PATCH 01/55] ci: fix release build and nexus upload pipeline after 1.6 (#2444) --- .github/workflows/maint-cleanup-releases.yaml | 5 ++++- .github/workflows/nexus-upload.yaml | 18 +++++++++++----- .github/workflows/release-build.yaml | 18 +++++++++------- .github/workflows/release-hotfix.yaml | 11 +++------- .github/workflows/release-semantic.yaml | 21 +++++++++++++++++++ .../Shaders/Features/TerrainHelper.ini | 3 ++- tools/feature_version_audit.py | 17 ++++++++++++++- 7 files changed, 69 insertions(+), 24 deletions(-) diff --git a/.github/workflows/maint-cleanup-releases.yaml b/.github/workflows/maint-cleanup-releases.yaml index e934cf7085..069b78eced 100644 --- a/.github/workflows/maint-cleanup-releases.yaml +++ b/.github/workflows/maint-cleanup-releases.yaml @@ -57,7 +57,10 @@ jobs: # Get all git tags that look like pre-releases but may not have GitHub releases git fetch --tags - orphaned_tags=$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-(pr[0-9]+|rc[0-9]+|alpha[0-9]*|beta[0-9]*|dev[0-9]*)' || true) + # Optional '.' before the number so semantic-release's dotted + # prerelease tags (e.g. v1.6.0-rc.1) are matched, not just + # v1.6.0-rc1. + orphaned_tags=$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+-(pr|rc|alpha|beta|dev)\.?[0-9]+' || true) # Combine both lists and remove duplicates all_prerelease_tags=$(echo -e "$releases\n$orphaned_tags" | sort | uniq | grep -v '^$' || true) diff --git a/.github/workflows/nexus-upload.yaml b/.github/workflows/nexus-upload.yaml index cfff0dd547..aa27d0da8e 100644 --- a/.github/workflows/nexus-upload.yaml +++ b/.github/workflows/nexus-upload.yaml @@ -497,11 +497,19 @@ jobs: fi echo "file=$FILE" >> $GITHUB_OUTPUT LINK="https://github.com/${REPO}/releases/tag/${RELEASE_TAG}" - if [ -n "$FILE_DESC" ]; then - printf 'description=%s Full changelog: %s\n' "$FILE_DESC" "$LINK" >> $GITHUB_OUTPUT - else - printf 'description=Full changelog: %s\n' "$LINK" >> $GITHUB_OUTPUT - fi + # file_description is multi-line (per-feature compatibility + # bullets), so write it with the heredoc-delimiter form. A plain + # 'description=...' breaks $GITHUB_OUTPUT on the embedded newlines + # ("Error: Invalid format '• HDR 1.0.2'"). + { + echo 'description<> "$GITHUB_OUTPUT" - name: Validate file_group_id if: steps.version-check.outputs.should_upload == 'true' diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index 72bbcbcf6f..b75b8f81b4 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -35,6 +35,10 @@ concurrency: jobs: build: name: Build + # Build on dispatch / tag push / a non-draft `release: created`, but NOT + # on `release: published`: the artifacts were already built and attached + # when the draft was created, so publishing must not rebuild. + if: github.event.action != 'published' uses: ./.github/workflows/_shared-build.yaml with: ref: ${{ github.ref }} @@ -46,7 +50,7 @@ jobs: feature-audit: name: Feature Version Audit (Release) - if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release' + if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release') && github.event.action != 'published' runs-on: ubuntu-latest continue-on-error: true steps: @@ -78,6 +82,7 @@ jobs: if: > always() && !cancelled() && needs.build.outputs.cpp-built == 'true' && + github.event.action != 'published' && (github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))) @@ -187,16 +192,13 @@ jobs: allowUpdates: true nexus-dry-run: - # Chained from `release` to avoid the artifact-attachment race a - # parallel `release: published` trigger on a separate workflow had: - # the prior release-nexus-trigger.yaml fired in parallel with this - # build and tried to download artifacts before they existed. + # Runs only on publish, independently of the build jobs (which are + # skipped on publish). Artifacts were already built and attached to the + # release when its draft was created, and nexus-upload downloads them + # from the release assets — so there is nothing to wait on and no rebuild. # Stable tags only (no '-' in the tag name). name: Nexus Upload (dry run) - needs: [release] if: > - always() && !cancelled() && - needs.release.result == 'success' && github.event_name == 'release' && github.event.action == 'published' && !contains(github.event.release.tag_name, '-') diff --git a/.github/workflows/release-hotfix.yaml b/.github/workflows/release-hotfix.yaml index 1125bea95c..3add2299f2 100644 --- a/.github/workflows/release-hotfix.yaml +++ b/.github/workflows/release-hotfix.yaml @@ -286,14 +286,9 @@ jobs: - name: Open PR against hotfix branch if: ${{ !inputs.dry_run && steps.cherry.outputs.picked_count != '0' }} env: - # Use GITHUB_TOKEN (not RELEASE_PAT). The workflow declares - # `permissions: pull-requests: write`, which is sufficient - # for creating the candidate PR — no branch-protection - # bypass is required here, so there's no need to spend - # RELEASE_PAT's broader scope (and avoids depending on the - # PAT having pull_requests:write granted, which a previous - # run hit: "Resource not accessible by personal access - # token (createPullRequest)"). + # GITHUB_TOKEN suffices here: the workflow declares + # `permissions: pull-requests: write`, and creating the + # candidate PR requires no branch-protection bypass. GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Pass via env (not ${{ }} interpolation) so the literal # backticks inside SOURCE_DESC don't get re-evaluated by diff --git a/.github/workflows/release-semantic.yaml b/.github/workflows/release-semantic.yaml index 13cf0edc82..9911c4159a 100644 --- a/.github/workflows/release-semantic.yaml +++ b/.github/workflows/release-semantic.yaml @@ -19,6 +19,9 @@ on: permissions: contents: write + # actions:write lets this job dispatch release-build.yaml after cutting a + # release (the tag push can't trigger it — see the step below). + actions: write concurrency: # Serialize promotions so two concurrent dispatches on main can't race @@ -173,6 +176,24 @@ jobs: # (release-build.yaml); GITHUB_TOKEN pushes do not. GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Build artifacts for the new release + # The release tag does NOT auto-trigger release-build.yaml: it sits + # on a `chore(release): … [skip ci]` commit (the tag push is skipped) + # and the GitHub release is created as a draft (no `release: created` + # event fires). So dispatch the build explicitly here to populate the + # draft with .7z artifacts. Publishing the draft later does NOT + # rebuild — release-build.yaml skips its build job on the + # `release: published` event — it just publishes the already-attached + # artifacts and fires the Nexus dry-run via `release: published`. + if: steps.semantic.outputs.new_release_published == 'true' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.semantic.outputs.new_release_git_tag }} + run: | + gh workflow run release-build.yaml \ + --repo "${{ github.repository }}" \ + --ref "${TAG}" + - name: Reconcile dev with release commit (promotion mode, dev source only) if: ${{ inputs.ff_target != '' && steps.semantic.outputs.new_release_published == 'true' && steps.validate.outputs.source == 'dev' }} env: diff --git a/features/Terrain Helper/Shaders/Features/TerrainHelper.ini b/features/Terrain Helper/Shaders/Features/TerrainHelper.ini index 9dbee137a6..321113bad1 100644 --- a/features/Terrain Helper/Shaders/Features/TerrainHelper.ini +++ b/features/Terrain Helper/Shaders/Features/TerrainHelper.ini @@ -1,8 +1,9 @@ [Info] Version = 1-0-1 +AuditVersion = false [Nexus] nexusmodid = 143149 nexusfilegroupid = 000000 nexusfilename = Terrain Helper -autoupload = true +autoupload = false diff --git a/tools/feature_version_audit.py b/tools/feature_version_audit.py index e9959a90cb..08bfd1e48a 100644 --- a/tools/feature_version_audit.py +++ b/tools/feature_version_audit.py @@ -186,7 +186,7 @@ def get_feature_ini_metadata(feature_dir_or_ini_path): if not sections: sections = ['Info'] if parser.has_section('Info') else [] - metadata = {'auto_upload': False} + metadata = {'auto_upload': False, 'audit_version': True} for section in sections: if not parser.has_section(section): continue @@ -196,6 +196,15 @@ def get_feature_ini_metadata(feature_dir_or_ini_path): if auto_upload_str is not None: metadata['auto_upload'] = str(auto_upload_str).strip().lower() not in ('false', '0', 'no', 'off', '') + # Activation-only features keep all code in the core mod; their toggle + # .ini opts out of version auditing/bumping with `AuditVersion = false`. + # Auditing is on by default, so a blank value must NOT exclude the ini: + # only explicit falsy values opt out (unlike auto_upload, which is opt-in + # and treats blank as off). + audit_version_str = section_items.get('auditversion') or section_items.get('audit_version') + if audit_version_str is not None: + metadata['audit_version'] = str(audit_version_str).strip().lower() not in ('false', '0', 'no', 'off') + section_metadata = { 'mod_id': section_items.get('nexusmodid') or section_items.get('nexus_mod_id') or section_items.get('mod_id'), 'mod_filename': section_items.get('nexusfilename') or section_items.get('nexus_filename') or section_items.get('nexusmodfilename') or section_items.get('nexus_mod_filename') or section_items.get('mod_filename') or section_items.get('modname') or section_items.get('name'), @@ -630,6 +639,12 @@ def get_feature_key(feature_dir, feature_meta_map): meta = feature_meta_map.get(feature_key) ini_path = get_feature_ini(feature_dir) + # Activation-only features (e.g. Terrain Helper) opt out of version + # auditing via `AuditVersion = false` in their .ini: all their code lives + # in the core mod, so bumping the toggle .ini on every edit is meaningless + # churn. Skip them entirely from bump suggestions and PR-check failures. + if ini_path and not get_feature_ini_metadata(ini_path).get('audit_version', True): + continue # Use last release tag (version_ref) as the baseline for version proposals so that # multiple PRs between releases don't accumulate spurious bumps. prior_ver = get_prior_version(ini_path, version_ref) if ini_path else None From 660fc51b08c09f48456c9fa1e792799652a8da9a Mon Sep 17 00:00:00 2001 From: davo0411 Date: Sun, 31 May 2026 22:07:06 +1000 Subject: [PATCH 02/55] feat(screenshots): HDR & SDR .png, clipboard support (#2434) --- CMakeLists.txt | 7 + extern/sk_hdr_png/LICENSE | 28 + extern/sk_hdr_png/README.md | 8 + extern/sk_hdr_png/include/sk_hdr_png.hpp | 1077 +++++++++++++++++ .../Shaders/Features/Screenshot.ini | 2 +- src/Features/ScreenshotFeature.cpp | 431 ++++++- src/Features/ScreenshotFeature.h | 12 + src/Hooks.cpp | 3 + 8 files changed, 1500 insertions(+), 68 deletions(-) create mode 100644 extern/sk_hdr_png/LICENSE create mode 100644 extern/sk_hdr_png/README.md create mode 100644 extern/sk_hdr_png/include/sk_hdr_png.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e79658648b..da26c79aae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,9 +120,15 @@ target_include_directories( ${BSHOSHANY_THREAD_POOL_INCLUDE_DIRS} ${CLIB_UTIL_INCLUDE_DIRS} "${CMAKE_SOURCE_DIR}/package/Shaders" + "${CMAKE_SOURCE_DIR}/extern/sk_hdr_png/include" ${DETOURS_INCLUDE_DIRS} ) +target_compile_definitions( + ${PROJECT_NAME} + PRIVATE SK_HDR_PNG_COMMUNITY_SHADERS +) + target_link_libraries( ${PROJECT_NAME} PRIVATE @@ -142,6 +148,7 @@ target_link_libraries( d3d12.lib Microsoft::DirectX-Headers ${DETOURS_LIBRARY} + windowscodecs ) # Some third-party libs (e.g. FFX backend) are built with /GL. diff --git a/extern/sk_hdr_png/LICENSE b/extern/sk_hdr_png/LICENSE new file mode 100644 index 0000000000..065d15326e --- /dev/null +++ b/extern/sk_hdr_png/LICENSE @@ -0,0 +1,28 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +Source: sk_hdr_png.hpp from ReShade (commit e7cce821877b8a195fcc842febbcc7e0d8edd86b) +https://github.com/crosire/reshade/commit/e7cce821877b8a195fcc842febbcc7e0d8edd86b +Author: Kaldaien (Special K) diff --git a/extern/sk_hdr_png/README.md b/extern/sk_hdr_png/README.md new file mode 100644 index 0000000000..6a45b70cbe --- /dev/null +++ b/extern/sk_hdr_png/README.md @@ -0,0 +1,8 @@ +# sk_hdr_png + +HDR10/scRGB screenshot encoding as PNG with HDR metadata chunks (cHRM, mDCv, cLLi, iCCP). + +Vendored from [ReShade](https://github.com/crosire/reshade) commit +[e7cce82](https://github.com/crosire/reshade/commit/e7cce821877b8a195fcc842febbcc7e0d8edd86b). + +Licensed under the [Unlicense](LICENSE) (public domain). diff --git a/extern/sk_hdr_png/include/sk_hdr_png.hpp b/extern/sk_hdr_png/include/sk_hdr_png.hpp new file mode 100644 index 0000000000..b63e5d8600 --- /dev/null +++ b/extern/sk_hdr_png/include/sk_hdr_png.hpp @@ -0,0 +1,1077 @@ +// This is free and unencumbered software released into the public domain. +// +// Anyone is free to copy, modify, publish, use, compile, sell, or +// distribute this software, either in source code form or as a compiled +// binary, for any purpose, commercial or non-commercial, and by any +// means. +// +// In jurisdictions that recognize copyright laws, the author or authors +// of this software dedicate any and all copyright interest in the +// software to the public domain. We make this dedication for the benefit +// of the public at large and to the detriment of our heirs and +// successors. We intend this dedication to be an overt act of +// relinquishment in perpetuity of all present and future rights to this +// software under copyright law. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// For more information, please refer to +#pragma once + +#ifdef SK_HDR_PNG_COMMUNITY_SHADERS +#include +namespace sk_hdr_png +{ + enum class format : uint32_t + { + r16g16b16a16_float, // scRGB / scene-linear BT.709 (ReShade back buffer) + r16g16b16a16_pq, // BT.2020 PQ already applied (Community Shaders HDROutputCS) + r10g10b10a2_unorm, + b10g10r10a2_unorm, + }; + + template + using com_ptr = winrt::com_ptr; +} +#else +#include "reshade_api.hpp" +//#include "reshade_api_display.hpp" +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#pragma comment (lib, "windowscodecs.lib") + +// Use AVX for SIMD fp16 to fp32 conversion +#pragma push_macro("_XM_F16C_INTRINSICS_") +#if (defined _M_IX86) || (defined _M_X64) +#undef _XM_F16C_INTRINSICS_ +#define _XM_F16C_INTRINSICS_ +#endif + +#include +#include +#include + +namespace sk_hdr_png +{ +#ifndef SK_HDR_PNG_COMMUNITY_SHADERS + using format = reshade::api::format; +#endif + +#define DeclareUint32(x,y) uint32_t x = SetUint32((x),(y)) +#if (defined _M_IX86) || (defined _M_X64) + uint32_t GetUint32 (uint32_t x) noexcept { return _byteswap_ulong (x); }; + uint32_t SetUint32 (uint32_t& x, uint32_t y) noexcept { x = _byteswap_ulong (y); return x; }; +#else + uint32_t GetUint32 (uint32_t x) noexcept { return x; }; + uint32_t SetUint32 (uint32_t& x, uint32_t y) noexcept { x = y; return x; }; +#endif + + struct cHRM_Payload + { + DeclareUint32 (white_x, 31270); + DeclareUint32 (white_y, 32900); + DeclareUint32 (red_x, 70800); + DeclareUint32 (red_y, 29200); + DeclareUint32 (green_x, 17000); + DeclareUint32 (green_y, 79700); + DeclareUint32 (blue_x, 13100); + DeclareUint32 (blue_y, 4600); + }; + + struct sBIT_Payload + { + uint8_t red_bits = 10; // May be up to 16, for scRGB data + uint8_t green_bits = 10; // May be up to 16, for scRGB data + uint8_t blue_bits = 10; // May be up to 16, for scRGB data + }; + + struct mDCv_Payload + { + struct { + DeclareUint32 (red_x, 35400); // 0.708 / 0.00002 + DeclareUint32 (red_y, 14600); // 0.292 / 0.00002 + DeclareUint32 (green_x, 8500); // 0.17 / 0.00002 + DeclareUint32 (green_y, 39850); // 0.797 / 0.00002 + DeclareUint32 (blue_x, 6550); // 0.131 / 0.00002 + DeclareUint32 (blue_y, 2300); // 0.046 / 0.00002 + } primaries; + + struct { + DeclareUint32 (x, 15635); // 0.3127 / 0.00002 + DeclareUint32 (y, 16450); // 0.3290 / 0.00002 + } white_point; + + // The only real data we need to fill-in + struct { + DeclareUint32 (maximum, 10000000); // 1000.0 cd/m^2 + DeclareUint32 (minimum, 1); // 0.0001 cd/m^2 + } luminance; + }; + + struct cLLi_Payload + { + DeclareUint32 (max_cll, 10000000); // 1000 / 0.0001 + DeclareUint32 (max_fall, 2500000); // 250 / 0.0001 + }; + + // + // ICC Profile for tonemapping comes courtesy of ledoge + // + // https://github.com/ledoge/jxr_to_png + // + struct iCCP_Payload + { + char profile_name [20] = "RGB_D65_202_Rel_PeQ"; + uint8_t compression_type = 0;// (PNG_COMPRESSION_TYPE_DEFAULT) + unsigned char profile_data [2178] = + { + 0x78, 0x9C, 0xED, 0x97, 0x79, 0x58, 0x13, 0x67, 0x1E, 0xC7, 0x47, 0x50, + 0x59, 0x95, 0x2A, 0xAC, 0xED, 0xB6, 0x8B, 0xA8, 0x54, 0x20, 0x20, 0x42, + 0xE5, 0xF4, 0x00, 0x51, 0x40, 0x05, 0xAF, 0x6A, 0x04, 0x51, 0x6E, 0x84, + 0x70, 0xAF, 0x20, 0x24, 0xDC, 0x87, 0x0C, 0xA8, 0x88, 0x20, 0x09, 0x90, + 0x04, 0x12, 0x24, 0x24, 0x90, 0x03, 0x82, 0xA0, 0x41, 0x08, 0x24, 0x41, + 0x2E, 0x21, 0x01, 0x12, 0x83, 0x4A, 0x10, 0xA9, 0x56, 0xB7, 0x8A, 0xE0, + 0xAD, 0x21, 0xE0, 0xB1, 0x6B, 0x31, 0x3B, 0x49, 0x74, 0x09, 0x6D, 0xD7, + 0x3E, 0xCF, 0x3E, 0xFD, 0xAF, 0x4E, 0x3E, 0xF3, 0xBC, 0xBF, 0x79, 0xBF, + 0xEF, 0xBC, 0x33, 0x9F, 0xC9, 0xFC, 0x31, 0x2F, 0x00, 0xE8, 0xBC, 0x8D, + 0x4A, 0x3E, 0x62, 0x30, 0xD7, 0x09, 0x00, 0xA2, 0x63, 0xE2, 0x91, 0xEE, + 0x6E, 0x2E, 0x06, 0x7B, 0x82, 0x82, 0x0D, 0xB4, 0x46, 0x01, 0x6D, 0x60, + 0x0E, 0xA0, 0xDC, 0x82, 0x10, 0xA8, 0x58, 0x67, 0x38, 0x7C, 0x8F, 0xEA, + 0xE8, 0x57, 0x1B, 0x34, 0xEA, 0xF5, 0xB0, 0x6A, 0xAC, 0xC4, 0x42, 0x31, + 0xD7, 0xF2, 0x84, 0x9D, 0x68, 0xBD, 0xA9, 0xD6, 0x43, 0xEB, 0x16, 0xE5, + 0xBD, 0xFC, 0xC6, 0xD2, 0xFC, 0xF8, 0xFF, 0x38, 0xEF, 0xE3, 0xB6, 0x30, + 0x24, 0x14, 0x85, 0x80, 0xDA, 0x9F, 0xA1, 0x7D, 0x1B, 0x22, 0x16, 0x19, + 0x0F, 0x4D, 0xE9, 0x04, 0xD5, 0x46, 0x49, 0xF1, 0xB1, 0x8A, 0x3A, 0x04, + 0xAA, 0xBF, 0x44, 0x44, 0x04, 0x41, 0xED, 0x9C, 0x64, 0xA8, 0x36, 0x47, + 0x44, 0x22, 0x62, 0xA1, 0x9A, 0x06, 0xD5, 0xDA, 0x48, 0x2F, 0x6F, 0x1F, + 0xA8, 0x66, 0x29, 0xC6, 0x84, 0xAB, 0xEA, 0x1E, 0x45, 0x1D, 0xAC, 0xAA, + 0x47, 0x14, 0xB5, 0xB3, 0xB5, 0x8B, 0x25, 0x54, 0x3F, 0x03, 0x80, 0xC5, + 0x97, 0x5C, 0xAC, 0x9D, 0xA1, 0x5A, 0xA7, 0x06, 0xEA, 0x87, 0x47, 0x1F, + 0x49, 0x50, 0x5C, 0xF7, 0x83, 0x03, 0xA0, 0x1D, 0x1A, 0xE3, 0xE9, 0x01, + 0xB5, 0x30, 0x68, 0xD7, 0x07, 0xDC, 0x01, 0x37, 0xC0, 0x05, 0x08, 0x04, + 0xB6, 0x01, 0xEB, 0x00, 0x3B, 0xA8, 0xB5, 0x06, 0x2C, 0xA1, 0x3D, 0x10, + 0xEA, 0x0F, 0x05, 0x8E, 0x40, 0x2D, 0x1C, 0x6A, 0xF7, 0x43, 0xCF, 0xEC, + 0xB7, 0xE7, 0x98, 0xAF, 0x9C, 0x63, 0x2B, 0xF4, 0x83, 0xAE, 0x06, 0xDD, + 0x8A, 0x81, 0x6A, 0xC8, 0xCC, 0x73, 0x42, 0x85, 0xD9, 0x58, 0xAB, 0xCE, + 0xD2, 0x86, 0x5C, 0xE7, 0xDD, 0x91, 0xCB, 0x27, 0xCD, 0x00, 0x40, 0xAB, + 0x18, 0x00, 0xA6, 0x0B, 0xE5, 0xF2, 0x77, 0x54, 0xB9, 0x7C, 0x9A, 0x0A, + 0x00, 0x9A, 0xB7, 0x01, 0xA0, 0x33, 0x4B, 0xE5, 0x0B, 0x00, 0x0B, 0x74, + 0x80, 0x39, 0x33, 0x73, 0xD5, 0x45, 0x00, 0x80, 0xDB, 0x51, 0xB9, 0x5C, + 0x9E, 0x3D, 0xD3, 0x67, 0x16, 0x09, 0xF5, 0x8F, 0x42, 0xF3, 0xD4, 0xCF, + 0xF4, 0x19, 0x68, 0x01, 0xC0, 0xA2, 0xF3, 0x00, 0x70, 0x65, 0x69, 0x74, + 0x58, 0xBC, 0x95, 0xA2, 0x47, 0x53, 0x73, 0x81, 0xEA, 0x6E, 0x7F, 0xF1, + 0x2F, 0xFE, 0xEA, 0x78, 0x8E, 0x86, 0xE6, 0xDC, 0x79, 0xF3, 0xB5, 0xFE, + 0xB2, 0x60, 0xE1, 0x22, 0xED, 0x2F, 0x16, 0x2F, 0xD1, 0xD1, 0xFD, 0xEB, + 0xD2, 0x2F, 0xBF, 0xFA, 0xDB, 0xD7, 0xDF, 0xFC, 0x5D, 0x6F, 0x99, 0xFE, + 0xF2, 0x15, 0x2B, 0x0D, 0xBE, 0x5D, 0x65, 0x68, 0x64, 0x0C, 0x33, 0x31, + 0x5D, 0x6D, 0xB6, 0xC6, 0xDC, 0xE2, 0xBB, 0xB5, 0x96, 0x56, 0xD6, 0x36, + 0xB6, 0x76, 0xEB, 0xD6, 0x6F, 0xD8, 0x68, 0xEF, 0xB0, 0xC9, 0x71, 0xF3, + 0x16, 0x27, 0x67, 0x97, 0xAD, 0xDB, 0xB6, 0xBB, 0xBA, 0xED, 0xD8, 0xB9, + 0x6B, 0xF7, 0x9E, 0xEF, 0xF7, 0xEE, 0x83, 0xEF, 0x77, 0xF7, 0x38, 0xE0, + 0x79, 0xF0, 0x10, 0x74, 0x6F, 0xBE, 0x7E, 0xFE, 0x01, 0x81, 0x87, 0x83, + 0x82, 0x11, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xFF, 0x38, 0x12, + 0x1D, 0x73, 0x34, 0x36, 0x0E, 0x89, 0x8A, 0x4F, 0x48, 0x4C, 0x4A, 0x4E, + 0x49, 0x4D, 0x4B, 0xCF, 0x38, 0x96, 0x09, 0x66, 0x65, 0x1F, 0x3F, 0x71, + 0x32, 0xE7, 0x54, 0xEE, 0xE9, 0xBC, 0xFC, 0x33, 0x05, 0x68, 0x4C, 0x61, + 0x51, 0x31, 0x16, 0x87, 0x2F, 0x29, 0x25, 0x10, 0xCB, 0xCE, 0x96, 0x93, + 0x2A, 0xC8, 0x94, 0xCA, 0x2A, 0x2A, 0x8D, 0xCE, 0xA8, 0xAE, 0x61, 0xD6, + 0x9E, 0xAB, 0xAB, 0x3F, 0x7F, 0x81, 0xD5, 0x70, 0xB1, 0xB1, 0x89, 0xDD, + 0xDC, 0xC2, 0xE1, 0xF2, 0x5A, 0x2F, 0xB5, 0xB5, 0x77, 0x74, 0x76, 0x5D, + 0xEE, 0xEE, 0xE1, 0x0B, 0x7A, 0xFB, 0xFA, 0x85, 0xA2, 0x2B, 0xE2, 0x81, + 0xAB, 0xD7, 0xAE, 0x0F, 0x4A, 0x86, 0x6E, 0x0C, 0xDF, 0x1C, 0xF9, 0xE1, + 0xD6, 0xED, 0x1F, 0xEF, 0xDC, 0xFD, 0xE7, 0x4F, 0xF7, 0xEE, 0x8F, 0x3E, + 0x18, 0x1B, 0x7F, 0xF8, 0xE8, 0xF1, 0x93, 0xA7, 0xCF, 0x9E, 0xBF, 0x78, + 0x29, 0x9D, 0x90, 0x4D, 0x4E, 0xBD, 0x7A, 0xFD, 0xE6, 0xED, 0xBF, 0xFE, + 0xFD, 0xEE, 0xE7, 0xE9, 0xF7, 0xF2, 0xCF, 0xFE, 0x7F, 0x72, 0x7F, 0x10, + 0x04, 0xB2, 0x32, 0x34, 0x4E, 0x85, 0x2F, 0xAC, 0x70, 0x33, 0x64, 0x1B, + 0xEF, 0xE8, 0x99, 0x1F, 0x7A, 0x41, 0x2F, 0xAE, 0x66, 0x55, 0x22, 0x1D, + 0x36, 0x37, 0x2D, 0x7B, 0x6E, 0x7A, 0xE6, 0xFC, 0xEC, 0xC8, 0xC5, 0xC4, + 0x5D, 0xC6, 0x17, 0x4D, 0x76, 0x76, 0x6B, 0x41, 0x11, 0x52, 0x19, 0xAD, + 0xF0, 0xC1, 0xAE, 0xF4, 0x2D, 0x34, 0x08, 0x4E, 0x35, 0x4E, 0xF3, 0xB6, + 0x25, 0x59, 0xC1, 0xB9, 0xDA, 0x11, 0x75, 0xFA, 0xC8, 0x6A, 0xC3, 0x24, + 0x3A, 0x6C, 0xDF, 0x3A, 0xD6, 0xBE, 0xF5, 0x75, 0x70, 0x07, 0x82, 0xFB, + 0x9E, 0x44, 0xAF, 0xA3, 0x3B, 0x23, 0xCE, 0xEA, 0xC7, 0x51, 0x57, 0x25, + 0xD2, 0x8C, 0x93, 0x69, 0x26, 0xE8, 0x25, 0x62, 0xF4, 0x12, 0x21, 0x5A, + 0xF7, 0x12, 0x46, 0x8F, 0x5C, 0x64, 0x15, 0x87, 0x0F, 0xB1, 0xCF, 0x43, + 0xDB, 0x80, 0xE5, 0xE6, 0x69, 0x95, 0xAB, 0xF9, 0xC0, 0x03, 0x3E, 0x30, + 0xCA, 0x07, 0x7E, 0xE4, 0x03, 0x7D, 0x02, 0x80, 0xD6, 0xAB, 0x17, 0xCD, + 0x8E, 0xD9, 0x57, 0x96, 0xE7, 0x98, 0x43, 0xB4, 0x1C, 0x33, 0x6C, 0x52, + 0xD2, 0x38, 0x66, 0xD8, 0x30, 0x66, 0x54, 0x3B, 0x6E, 0x4A, 0x78, 0x68, + 0x1D, 0xDB, 0x83, 0xF2, 0x26, 0xE7, 0x39, 0x49, 0xF7, 0x13, 0x3F, 0x42, + 0x90, 0xEE, 0x2F, 0x95, 0xBA, 0xE3, 0xA5, 0x07, 0xCE, 0xC8, 0x7C, 0x93, + 0xFA, 0xE2, 0xFD, 0x64, 0xDE, 0xB8, 0x59, 0xF8, 0x60, 0x65, 0x3E, 0x45, + 0x93, 0x7E, 0x79, 0xAF, 0x10, 0x29, 0x1A, 0x27, 0xB2, 0x34, 0x4E, 0x1E, + 0x9B, 0x9B, 0x1F, 0xA1, 0x5D, 0xB9, 0xC3, 0xA8, 0x19, 0xB6, 0x83, 0xAF, + 0xF0, 0x52, 0x29, 0xCF, 0xCB, 0x3C, 0x3E, 0x0F, 0x04, 0xB5, 0x72, 0xA2, + 0x74, 0xCA, 0x77, 0xC3, 0x2E, 0x9A, 0xAA, 0x2B, 0xAF, 0x0C, 0xC4, 0x19, + 0x1C, 0x2E, 0xFA, 0x36, 0x3C, 0x0D, 0x96, 0xE1, 0x63, 0x57, 0x61, 0x05, + 0xE7, 0xA9, 0x29, 0x6F, 0x64, 0xED, 0xB3, 0xAF, 0x87, 0x6F, 0x26, 0xB8, + 0xEF, 0x4D, 0xF2, 0x8E, 0x9D, 0xAD, 0xAC, 0x23, 0x46, 0xEB, 0x0A, 0xD1, + 0x4B, 0xDB, 0x30, 0xCB, 0xC8, 0x45, 0xD6, 0xC8, 0x12, 0xA5, 0x72, 0x56, + 0xB9, 0x79, 0xFA, 0x27, 0x94, 0xCB, 0xFE, 0x78, 0xE5, 0x2F, 0x2A, 0x4E, + 0x2F, 0x26, 0xE7, 0x2C, 0xA9, 0x8A, 0xFD, 0xBA, 0x16, 0xBE, 0x86, 0xBB, + 0x66, 0xB7, 0x60, 0x41, 0x18, 0x6B, 0x19, 0xB2, 0xC6, 0x10, 0xF2, 0xD2, + 0x25, 0xE6, 0xEB, 0x96, 0xE5, 0x2E, 0x2D, 0x47, 0x2E, 0xA3, 0xBB, 0x5B, + 0x34, 0x9B, 0xEF, 0xE1, 0x2F, 0x08, 0xBB, 0xF0, 0x21, 0x32, 0x49, 0x27, + 0x9A, 0xA4, 0x97, 0x98, 0x82, 0xA0, 0xC5, 0x99, 0x80, 0x8D, 0x34, 0x5B, + 0x8F, 0xD6, 0xC5, 0x91, 0xF5, 0xFA, 0x28, 0xA5, 0xB2, 0xFB, 0xF7, 0x17, + 0xDD, 0xF7, 0x5E, 0xF0, 0xD8, 0x5F, 0xE6, 0xE9, 0x97, 0xE2, 0x9B, 0xB4, + 0x3B, 0xAA, 0x62, 0x39, 0x92, 0x66, 0x98, 0x48, 0x83, 0x41, 0xCA, 0x18, + 0xBD, 0x01, 0xCC, 0x32, 0x11, 0x46, 0xBF, 0xAD, 0xD0, 0x88, 0x52, 0xBC, + 0x01, 0x59, 0x12, 0xEE, 0x90, 0x8F, 0x81, 0x94, 0x2D, 0xD4, 0x94, 0xEF, + 0xF0, 0x81, 0x7E, 0xC1, 0x1C, 0x5A, 0xEF, 0xF2, 0x98, 0xE6, 0xA3, 0xF0, + 0xDF, 0x50, 0x36, 0x3E, 0xF7, 0x69, 0xE5, 0x09, 0xCF, 0xDF, 0x57, 0xB6, + 0x6C, 0xAE, 0xB4, 0x6A, 0xAE, 0xB0, 0x6A, 0x39, 0x65, 0xC7, 0x0B, 0x71, + 0xEA, 0xDE, 0x78, 0x48, 0xA4, 0x1B, 0xD5, 0xB0, 0x1C, 0x55, 0x63, 0x94, + 0xC4, 0x80, 0x59, 0x37, 0x56, 0x59, 0x37, 0x91, 0x6D, 0x9A, 0x72, 0xD7, + 0x73, 0x42, 0x9D, 0x2F, 0xDB, 0x7B, 0x09, 0xA1, 0x68, 0x85, 0x2A, 0x72, + 0xA4, 0x32, 0x37, 0x53, 0xE9, 0x9B, 0xE9, 0x18, 0x67, 0xE6, 0x91, 0x5D, + 0x6C, 0x27, 0xFF, 0xCB, 0x5F, 0x45, 0x9F, 0x5F, 0x19, 0x0F, 0x45, 0x74, + 0x58, 0x40, 0x0A, 0x2F, 0x20, 0xA5, 0x25, 0x20, 0xAD, 0xEA, 0x30, 0x08, + 0x86, 0x62, 0xDC, 0x15, 0x2F, 0x0C, 0xC3, 0x18, 0xEA, 0x87, 0x94, 0xB1, + 0x0E, 0xD7, 0xB1, 0x0E, 0x03, 0xD8, 0x4D, 0x5D, 0x38, 0x67, 0x6A, 0x09, + 0x3C, 0xB1, 0x0C, 0xE5, 0x58, 0x50, 0x64, 0x97, 0x4D, 0xB2, 0x48, 0xAF, + 0x5A, 0x2D, 0xD0, 0x18, 0x13, 0x68, 0x3C, 0x10, 0x68, 0xDE, 0xED, 0x9D, + 0x2F, 0xEC, 0x5D, 0x42, 0xEF, 0x33, 0x3D, 0xDA, 0x12, 0x07, 0x2F, 0xCB, + 0xDF, 0x7C, 0x0A, 0x52, 0x36, 0x66, 0x8F, 0x19, 0x37, 0x29, 0xB9, 0x38, + 0x0E, 0x3B, 0x37, 0x6E, 0x46, 0x7C, 0x64, 0xAB, 0x52, 0x76, 0x9E, 0xA5, + 0xEC, 0xFE, 0x51, 0xD9, 0x2F, 0xA9, 0x2F, 0x61, 0xB6, 0xB2, 0xCF, 0x2C, + 0xE5, 0xE0, 0xA1, 0xEE, 0xE0, 0xA1, 0xCE, 0xE0, 0x21, 0x66, 0xC8, 0xF0, + 0x89, 0xC8, 0x5B, 0x9E, 0xF1, 0x23, 0x46, 0x49, 0x6C, 0x58, 0x72, 0xAD, + 0x49, 0x32, 0xC3, 0x04, 0x21, 0xE9, 0x41, 0x48, 0x3A, 0x11, 0x12, 0x66, + 0xE8, 0x8D, 0x93, 0x51, 0x3F, 0x78, 0x26, 0x8C, 0x18, 0xFF, 0x37, 0x8A, + 0x10, 0x09, 0x22, 0x44, 0xDD, 0x11, 0x57, 0xEA, 0xA2, 0xC4, 0xB9, 0x31, + 0x83, 0x5E, 0xC9, 0x83, 0x26, 0x29, 0x8D, 0x26, 0x29, 0x4C, 0x93, 0x14, + 0x86, 0x49, 0x1A, 0xEB, 0x6A, 0x1A, 0x4B, 0x94, 0xD6, 0xD0, 0x9C, 0xDE, + 0x88, 0xCD, 0xE4, 0x86, 0xE4, 0x74, 0x5A, 0x82, 0x75, 0xE6, 0xE9, 0x8C, + 0xD5, 0xA9, 0x74, 0x53, 0x72, 0xE2, 0x6D, 0x72, 0xE2, 0x08, 0x39, 0x51, + 0x48, 0x49, 0xA9, 0xAF, 0x04, 0x41, 0x3A, 0x76, 0x3B, 0x8E, 0x68, 0x9F, + 0x53, 0x61, 0x99, 0x51, 0x65, 0x26, 0x34, 0x7F, 0x2C, 0x34, 0x7F, 0x24, + 0x34, 0xBF, 0x27, 0xFC, 0x4E, 0x2C, 0xB4, 0x65, 0x8A, 0x5C, 0x51, 0xBC, + 0x64, 0x0F, 0x52, 0xC1, 0x96, 0xDC, 0x32, 0xAB, 0x87, 0x6B, 0x5A, 0x3E, + 0xC2, 0x7E, 0x68, 0x7E, 0xFE, 0xE1, 0xDA, 0xB3, 0x8F, 0x37, 0xC4, 0xF1, + 0x13, 0x7C, 0x28, 0xF9, 0xCE, 0x52, 0x0F, 0xA2, 0x1A, 0x04, 0xE9, 0x01, + 0xFC, 0xC4, 0xC1, 0x82, 0x49, 0xFF, 0x64, 0x85, 0xB2, 0x42, 0x53, 0x1D, + 0xAC, 0xCC, 0xB7, 0x68, 0xD2, 0x5F, 0xA1, 0x4C, 0x92, 0x8A, 0x49, 0x52, + 0x11, 0x49, 0x7A, 0x99, 0x24, 0xAD, 0x25, 0x4F, 0x66, 0xD1, 0xDE, 0x6D, + 0xC7, 0x76, 0x6C, 0x3C, 0x59, 0xBF, 0x36, 0xA3, 0xDA, 0x8C, 0xF4, 0x52, + 0x4C, 0x7A, 0x79, 0x85, 0xF4, 0xF2, 0x72, 0x85, 0x32, 0xA2, 0xAB, 0x45, + 0xE4, 0x67, 0x57, 0xC9, 0xCF, 0xC4, 0xE4, 0xE7, 0x3D, 0xE4, 0xE7, 0x75, + 0x95, 0xD2, 0x6C, 0xC6, 0x5B, 0x57, 0x5C, 0xBB, 0xFD, 0xC9, 0x7A, 0x4B, + 0x28, 0x62, 0xDC, 0xBB, 0xC5, 0xB8, 0x37, 0xC2, 0xB8, 0x2F, 0xAE, 0x1E, + 0x6D, 0xAC, 0x19, 0xCF, 0xAD, 0x7B, 0xB1, 0x9B, 0xC8, 0x71, 0xCC, 0xAD, + 0xB5, 0x3A, 0xC6, 0x58, 0xC3, 0x69, 0x93, 0x72, 0xDA, 0x5E, 0x70, 0xDA, + 0x7E, 0xE2, 0xB4, 0x77, 0x73, 0x3B, 0x09, 0xAD, 0x7D, 0xFE, 0xD5, 0x4C, + 0xD7, 0x42, 0xEA, 0xFA, 0x6C, 0xAA, 0x85, 0x04, 0x25, 0x93, 0xA0, 0x26, + 0x24, 0xA8, 0x27, 0x92, 0xF8, 0x9B, 0x92, 0xA4, 0xA6, 0x21, 0x10, 0xEC, + 0xC9, 0xF7, 0xA6, 0x61, 0xB7, 0xE5, 0x97, 0xDB, 0x3E, 0x71, 0xE8, 0x51, + 0xD2, 0xFD, 0x64, 0x53, 0xD7, 0x53, 0x47, 0xCE, 0xD3, 0x2D, 0xD4, 0x67, + 0xAE, 0xA8, 0xFE, 0x34, 0xBF, 0xAA, 0x82, 0xAD, 0x13, 0x07, 0x89, 0x33, + 0x1C, 0x22, 0x4C, 0x1C, 0xC2, 0xCB, 0x7C, 0x0A, 0xA6, 0x0E, 0x27, 0xF7, + 0x27, 0xF9, 0xCB, 0x7C, 0x71, 0xEA, 0x4C, 0xFA, 0x62, 0x27, 0xFD, 0x8A, + 0x26, 0x03, 0xF2, 0x5E, 0x85, 0xA4, 0x70, 0x06, 0x28, 0x9C, 0x01, 0xB2, + 0x92, 0x72, 0xEE, 0x00, 0xAE, 0xF5, 0x6A, 0x66, 0x97, 0xC4, 0xAB, 0x92, + 0xED, 0x92, 0x57, 0x6B, 0xF3, 0xA9, 0x48, 0x4C, 0x51, 0x42, 0xE6, 0x8A, + 0xCB, 0xB9, 0x62, 0x28, 0x02, 0xBB, 0x06, 0xBD, 0x2A, 0x9B, 0xB6, 0x42, + 0x51, 0x6B, 0x3F, 0x45, 0x09, 0xB9, 0x55, 0x48, 0xBA, 0x24, 0xC4, 0xB7, + 0x8B, 0xB2, 0xBA, 0xAF, 0x7A, 0x53, 0x1B, 0xB7, 0xE5, 0x33, 0x6D, 0xBA, + 0x3B, 0xAA, 0xBA, 0x3B, 0x2A, 0x95, 0x90, 0x7B, 0x3A, 0x4B, 0xF9, 0x5D, + 0x27, 0x84, 0x82, 0x80, 0x9A, 0xF3, 0x6E, 0x05, 0xD5, 0x76, 0xC3, 0x8C, + 0xEA, 0x0F, 0x54, 0xD3, 0x87, 0xAB, 0x2B, 0x6E, 0x32, 0x0B, 0x6E, 0xB1, + 0x22, 0x9A, 0x29, 0x70, 0x3C, 0xC5, 0x5E, 0x86, 0x94, 0x2B, 0x99, 0x96, + 0x21, 0xA7, 0x64, 0xA8, 0xBB, 0xB2, 0x44, 0xAE, 0x0C, 0x04, 0x07, 0x73, + 0x83, 0x99, 0xC5, 0x6E, 0x53, 0x41, 0x65, 0x6A, 0x10, 0x5F, 0x05, 0x97, + 0xBE, 0x42, 0x60, 0xDE, 0x44, 0xA6, 0x8A, 0xD3, 0x03, 0x27, 0x03, 0x70, + 0xEA, 0x4C, 0x05, 0x60, 0xA7, 0x02, 0x8B, 0xA6, 0x82, 0xF2, 0x5F, 0x87, + 0xA5, 0xF2, 0x3B, 0x4A, 0x95, 0x94, 0x28, 0xC1, 0x09, 0x3A, 0xD0, 0x7D, + 0x1D, 0x99, 0x03, 0x5D, 0x87, 0xE9, 0x0D, 0xDB, 0xF9, 0xED, 0xA5, 0x0A, + 0x3E, 0x11, 0xB5, 0x97, 0x28, 0x99, 0x89, 0x18, 0x0D, 0xDB, 0x05, 0x6D, + 0xA5, 0x4A, 0x4A, 0x94, 0xE0, 0x7A, 0xDB, 0xD0, 0xFD, 0xED, 0xE0, 0x40, + 0x67, 0x10, 0x83, 0xE5, 0xDA, 0xC7, 0x2B, 0xFD, 0x48, 0x49, 0x3F, 0x0F, + 0xD7, 0xCF, 0xC3, 0x88, 0x5A, 0xB3, 0xAE, 0xB7, 0x05, 0x43, 0xD6, 0xD7, + 0x58, 0xA5, 0x6A, 0xE0, 0xAF, 0xB3, 0x0A, 0x07, 0x1B, 0x8E, 0xDF, 0x6C, + 0x0A, 0xAB, 0xAF, 0xDD, 0x75, 0xFF, 0x2C, 0x41, 0x8D, 0xD2, 0xFB, 0x67, + 0xB1, 0xA3, 0xA4, 0xD3, 0xE3, 0x94, 0x58, 0x1E, 0xC9, 0x63, 0x3A, 0xB9, + 0x56, 0x0D, 0xE6, 0x74, 0x4A, 0xF5, 0x74, 0x2A, 0x65, 0x1A, 0x04, 0x6F, + 0x9C, 0x0A, 0x7D, 0x1D, 0x8E, 0x57, 0x03, 0xA7, 0x20, 0xA2, 0xF8, 0x4D, + 0xE4, 0x99, 0xB7, 0x31, 0x69, 0xFD, 0x5C, 0x9C, 0x1A, 0x58, 0x21, 0xB7, + 0x58, 0xC8, 0x2D, 0xB8, 0xC2, 0x03, 0x07, 0x5B, 0x11, 0xFF, 0x5F, 0x24, + 0xE4, 0xE2, 0xD4, 0x50, 0x44, 0x22, 0x28, 0xE2, 0x82, 0x83, 0x3C, 0x84, + 0x90, 0x83, 0x53, 0x03, 0x2B, 0xE2, 0x14, 0x8B, 0x38, 0x05, 0x62, 0x0E, + 0x28, 0xE1, 0x85, 0x88, 0x9B, 0x70, 0x6A, 0x60, 0xC5, 0xEC, 0xE2, 0x01, + 0x76, 0xC1, 0x35, 0x76, 0xD6, 0x8D, 0x96, 0xD0, 0xA1, 0x3A, 0xDC, 0x6C, + 0x8A, 0x6F, 0xD4, 0xA1, 0x87, 0xEB, 0x8F, 0xDF, 0xBE, 0x10, 0x39, 0x46, + 0xC0, 0xAB, 0x81, 0x1B, 0x23, 0x60, 0xC7, 0x88, 0x85, 0xE3, 0x65, 0xB9, + 0x8F, 0x49, 0xC8, 0xF7, 0xE9, 0x25, 0x6A, 0xE0, 0x95, 0x60, 0xDF, 0x67, + 0xA0, 0xE5, 0xD0, 0x77, 0xC8, 0xE7, 0x4F, 0xD1, 0xCF, 0xFE, 0x7F, 0x66, + 0xFF, 0x68, 0x17, 0x67, 0xE5, 0x7A, 0x56, 0x53, 0x53, 0xB5, 0xA8, 0xFD, + 0xC5, 0x6A, 0x15, 0x88, 0x0D, 0x42, 0x06, 0xA9, 0xAF, 0x5D, 0x7F, 0xEF, + 0xF8, 0x3F, 0x0B, 0x10, 0x3B, 0xD9 + }; + }; + + bool write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard);//, reshade::api::display* display); + bool write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance_array, int quantization_bits);//, reshade::api::display* display); + cLLi_Payload calculate_content_light_info (const float* luminance, unsigned int width, unsigned int height); + bool copy_to_clipboard (const wchar_t* image_path); + bool remove_chunk (const char* chunk_name, void* data, size_t& size); + uint32_t crc32 (const void* typeless_data, size_t offset, size_t len, uint32_t crc); + + struct ParamsPQ + { + DirectX::XMVECTOR N, M; + DirectX::XMVECTOR C1, C2, C3; + DirectX::XMVECTOR MaxPQ; + DirectX::XMVECTOR RcpN, RcpM; + }; + + static const ParamsPQ PQ = + { + DirectX::XMVectorReplicate (2610.0 / 4096.0 / 4.0), // N + DirectX::XMVectorReplicate (2523.0 / 4096.0 * 128.0), // M + DirectX::XMVectorReplicate (3424.0 / 4096.0), // C1 + DirectX::XMVectorReplicate (2413.0 / 4096.0 * 32.0), // C2 + DirectX::XMVectorReplicate (2392.0 / 4096.0 * 32.0), // C3 + DirectX::XMVectorReplicate (125.0f), // MaxPQ + DirectX::XMVectorReciprocal (DirectX::XMVectorReplicate (2610.0 / 4096.0 / 4.0)), + DirectX::XMVectorReciprocal (DirectX::XMVectorReplicate (2523.0 / 4096.0 * 128.0)), + }; + + constexpr DirectX::XMMATRIX c_from709to2020 = + { + { 0.627403914928436279296875f, 0.069097287952899932861328125f, 0.01639143936336040496826171875f, 0.0f }, + { 0.3292830288410186767578125f, 0.9195404052734375f, 0.08801330626010894775390625f, 0.0f }, + { 0.0433130674064159393310546875f, 0.011362315155565738677978515625f, 0.895595252513885498046875f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + + constexpr DirectX::XMMATRIX c_from709toXYZ = + { + { 0.4123907983303070068359375f, 0.2126390039920806884765625f, 0.0193308182060718536376953125f, 0.0f }, + { 0.3575843274593353271484375f, 0.715168654918670654296875f, 0.119194783270359039306640625f, 0.0f }, + { 0.18048079311847686767578125f, 0.072192318737506866455078125f, 0.950532138347625732421875f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + + constexpr DirectX::XMMATRIX c_from2020toXYZ = + { + { 0.636958062648773193359375f, 0.26270020008087158203125f, 0.0f, 0.0f }, + { 0.144616901874542236328125f, 0.677998065948486328125f, 0.028072692453861236572265625f, 0.0f }, + { 0.1688809692859649658203125f, 0.0593017153441905975341796875f, 1.060985088348388671875f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + + static auto LinearToPQ = [](DirectX::FXMVECTOR N) + { + using namespace DirectX; + + XMVECTOR ret = + XMVectorPow (XMVectorDivide (XMVectorMax (N, g_XMZero), PQ.MaxPQ), PQ.N); + + XMVECTOR nd = + XMVectorDivide ( + XMVectorMultiplyAdd (PQ.C2, ret, PQ.C1), + XMVectorMultiplyAdd (PQ.C3, ret, g_XMOne) + ); + + return + XMVectorPow (nd, PQ.M); + }; + + static auto PQToLinear = [](DirectX::FXMVECTOR N) + { + using namespace DirectX; + + XMVECTOR ret = + XMVectorPow (XMVectorMax (N, g_XMZero), PQ.RcpM); + + XMVECTOR nd = + XMVectorDivide ( + XMVectorMax (XMVectorSubtract (ret, PQ.C1), g_XMZero), + XMVectorSubtract ( PQ.C2, + XMVectorMultiply (PQ.C3, ret))); + + ret = + XMVectorMultiply ( + XMVectorPow (nd, PQ.RcpN), PQ.MaxPQ + ); + + return ret; + }; +} + +sk_hdr_png::cLLi_Payload +sk_hdr_png::calculate_content_light_info (const float* luminance, unsigned int width, unsigned int height) +{ + using namespace DirectX; + using namespace DirectX::PackedVector; + + cLLi_Payload clli = { }; + + if (luminance == nullptr || width == 0 || height == 0) + return clli; + + float N = 0.0f; + float fLumAccum = 0.0f; + float fMaxLum = 0.0f; + float fMinLum = 5240320.0f; + + float fScanlineLum = 0.0f; + + const float* pixel_luminance = luminance; + + for (size_t y = 0; y < height; y++) + { + fScanlineLum = 0.0f; + + for (size_t x = 0; x < width ; x++) + { + fMaxLum = std::max (fMaxLum, *pixel_luminance); + fMinLum = std::min (fMinLum, *pixel_luminance); + + fScanlineLum += *pixel_luminance++; + } + + fLumAccum += + (fScanlineLum / static_cast (width)); + ++N; + } + + if (N > 0.0) + { + pixel_luminance = luminance; + + // 0 nits - 10k nits (appropriate for screencap, but not HDR photography) + fMinLum = std::clamp (fMinLum, 0.0f, 125.0f); + fMaxLum = std::clamp (fMaxLum, fMinLum, 125.0f); + + const float fLumRange = + (fMaxLum - fMinLum); + + auto luminance_freq = std::make_unique (65536); + ZeroMemory (luminance_freq.get (), sizeof (uint32_t) * 65536); + + for (size_t y = 0; y < height * width; y++) + { + luminance_freq [ + std::clamp ( (int) + std::roundf ( + (*pixel_luminance++ - fMinLum) / + (fLumRange / 65536.0f) ), + 0, 65535 ) ]++; + } + + double percent = 100.0; + const double img_size = (double)width * + (double)height; + + // Now that we have the frequency distribution, let's claim our prize... + // + // * Calculate the 99.5th percentile luminance and use it as MaxCLL + // + for (auto i = 65535; i >= 0; --i) + { + percent -= + 100.0 * ((double)luminance_freq [i] / img_size); + + if (percent <= 99.5) + { + fMaxLum = fMinLum + (fLumRange * ((float)i / 65536.0f)); + break; + } + } + + SetUint32 (clli.max_cll, + static_cast ((80.0f * fMaxLum ) / 0.0001f)); + SetUint32 (clli.max_fall, + static_cast ((80.0f * (fLumAccum / N)) / 0.0001f)); + } + + return clli; +} + +uint32_t +sk_hdr_png::crc32 (const void* typeless_data, size_t offset, size_t len, uint32_t crc) +{ + auto data = reinterpret_cast(typeless_data); + + if (data == nullptr || len == 0) + { + return static_cast(-1); + } + + uint32_t c; + + static uint32_t + png_crc_table[256] = { }; + if (png_crc_table[ 0 ] == 0) + { + for (auto i = 0 ; i < 256 ; ++i) + { + c = i; + + for (auto j = 0 ; j < 8 ; ++j) + { + if ((c & 1) == 1) c = (0xEDB88320 ^ ((c >> 1) & 0x7FFFFFFF)); + else c = ( (c >> 1) & 0x7FFFFFFF); + } + + png_crc_table [i] = c; + } + } + + c = (crc ^ 0xffffffff); + + for (auto k = offset ; k < (offset + len) ; ++k) + { + c = png_crc_table [(c ^ data [k]) & 255] ^ + ((c >> 8) & 0xFFFFFF); + } + + return (c ^ 0xffffffff); +} + +// +// To convert an image passed to an encoder that does not understand HDR, +// but that we actually fed HDR pixels to... perform the following: +// +// 1. Remove gAMA chunk (Prevents SKIV from recognizing as HDR) +// 2. Remove sRGB chunk (Prevents Discord from rendering in HDR) +// +// 3. Add cICP (The primary way of defining HDR10) +// 4. Add iCCP (Required for Discord to render in HDR) +// +// (5) Add cLLi [Unnecessary, but probably a good idea] +// (6) Add cHRM [Unnecessary, but probably a good idea] +// +bool +sk_hdr_png::remove_chunk (const char* chunk_name, void* data, size_t& size) +{ + if (chunk_name == nullptr || data == nullptr || size < 12 || strlen(chunk_name) < 4) + { + return false; + } + + size_t erase_pos = 0; + uint8_t* erase_ptr = nullptr; + + // Effectively a string search, but ignoring nul-bytes in both + // the character array being searched and the pattern... + std::string_view data_view((const char*)data, size); + if (erase_pos = data_view.find(chunk_name, 0, 4); + erase_pos == data_view.npos) + { + return false; + } + + erase_pos -= 4; // Rollback to the chunk's length field + erase_ptr = ((uint8_t*)data + erase_pos); + + uint32_t chunk_size = *(uint32_t*)erase_ptr; + + // Length is Big Endian, Intel/AMD CPUs are Little Endian +#if (defined _M_IX86) || (defined _M_X64) + chunk_size = _byteswap_ulong(chunk_size); +#endif + + size_t size_to_erase = (size_t)12 + chunk_size; + + memmove(erase_ptr, + erase_ptr + size_to_erase, + size - erase_pos - size_to_erase); + + size -= size_to_erase; + + return true; +} + +bool +sk_hdr_png::write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance, int quantization_bits)//, reshade::api::display* display) +{ + if (image_path == nullptr || width == 0 || height == 0 || quantization_bits < 6) + { + return false; + } + + // 16-byte alignment is mandatory for SIMD processing + if ((reinterpret_cast(luminance) & 0xF) != 0) + { + return false; + } + + FILE* + fPNG = _wfopen(image_path, L"r+b"); + if (fPNG != nullptr) + { + fseek(fPNG, 0, SEEK_END); + size_t size = ftell(fPNG); + rewind(fPNG); + + auto data = std::make_unique(size); + + if (! data) + { + fclose(fPNG); + return false; + } + + fread(data.get(), size, 1, fPNG); + rewind( fPNG); + + remove_chunk("sRGB", data.get(), size); + remove_chunk("gAMA", data.get(), size); + + fwrite(data.get(), size, 1, fPNG); + + // Truncate the file + _chsize(_fileno(fPNG), static_cast(size)); + + size_t insert_pos = 0; + const uint8_t* insert_ptr = nullptr; + + // Effectively a string search, but ignoring nul-bytes in both + // the character array being searched and the pattern... + std::string_view data_view((const char *)data.get(), size); + if (insert_pos = data_view.find("IDAT", 0, 4); + insert_pos == data_view.npos) + { + fclose(fPNG); + return false; + } + + insert_pos -= 4; // Rollback to the chunk's length field + insert_ptr = (data.get() + insert_pos); + + fseek(fPNG, static_cast(insert_pos), SEEK_SET); + + struct Chunk { + uint32_t len; + unsigned char name [4]; + void* data; + uint32_t crc; + uint32_t _native_len; + + void write (FILE* fStream) + { + // Length is Big Endian, Intel/AMD CPUs are Little Endian + if (_native_len == 0) + { + _native_len = len; +#if (defined _M_IX86) || (defined _M_X64) + len = _byteswap_ulong(_native_len); +#endif + } + + crc = crc32(data, 0, _native_len, crc32(name, 0, 4, 0x0)); + +#if (defined _M_IX86) || (defined _M_X64) + crc = _byteswap_ulong(crc); +#endif + + fwrite(&len, 8, 1, fStream); + fwrite(data, _native_len, 1, fStream); + fwrite(&crc, 4, 1, fStream); + }; + }; + + uint8_t cicp_data[] = { + 9, // BT.2020 Color Primaries + 16, // ST.2084 EOTF (PQ) + 0, // Identity Coefficients + 1, // Full Range + }; + + // Embedded ICC Profile so that Discord will render in HDR + iCCP_Payload iccp_data; + + cHRM_Payload chrm_data; // Rec 2020 chromaticity + sBIT_Payload sbit_data; // Bits in original source (max=16) + mDCv_Payload mdcv_data; // Display capabilities + cLLi_Payload clli_data; // Content light info + + clli_data = calculate_content_light_info(luminance, width, height); + + unsigned char sBIT_quantized = static_cast(quantization_bits); + sbit_data = { sBIT_quantized, sBIT_quantized, sBIT_quantized }; + + Chunk iccp_chunk = {sizeof(iCCP_Payload), {'i','C','C','P'}, &iccp_data}; + Chunk cicp_chunk = {sizeof(cicp_data), {'c','I','C','P'}, &cicp_data}; + Chunk clli_chunk = {sizeof(clli_data), {'c','L','L','i'}, &clli_data}; + Chunk sbit_chunk = {sizeof(sbit_data), {'s','B','I','T'}, &sbit_data}; + Chunk chrm_chunk = {sizeof(chrm_data), {'c','H','R','M'}, &chrm_data}; + + iccp_chunk.write(fPNG); + cicp_chunk.write(fPNG); + clli_chunk.write(fPNG); + sbit_chunk.write(fPNG); + chrm_chunk.write(fPNG); + +#if 0 + /// + /// Mastering metadata can be added, provided you are able to read this info + /// from the user's EDID. + /// + if (display != nullptr) + { + auto colorimetry = display->get_colorimetry(); + auto luminance_caps = display->get_luminance_caps(); + + sk_hdr_png::SetUint32 (mdcv_data.luminance.minimum, + static_cast (round (luminance_caps.min_nits / 0.0001f))); + sk_hdr_png::SetUint32 (mdcv_data.luminance.maximum, + static_cast (round (luminance_caps.max_nits / 0.0001f))); + + sk_hdr_png::SetUint32 (mdcv_data.primaries.red_x, + static_cast (round (colorimetry.red [0] / 0.00002))); + sk_hdr_png::SetUint32 (mdcv_data.primaries.red_y, + static_cast (round (colorimetry.red [1] / 0.00002))); + + sk_hdr_png::SetUint32 (mdcv_data.primaries.green_x, + static_cast (round (colorimetry.green [0] / 0.00002))); + sk_hdr_png::SetUint32 (mdcv_data.primaries.green_y, + static_cast (round (colorimetry.green [1] / 0.00002))); + + sk_hdr_png::SetUint32 (mdcv_data.primaries.blue_x, + static_cast (round (colorimetry.blue [0] / 0.00002))); + sk_hdr_png::SetUint32 (mdcv_data.primaries.blue_y, + static_cast (round (colorimetry.blue [1] / 0.00002))); + + sk_hdr_png::SetUint32 (mdcv_data.white_point.x, + static_cast (round (colorimetry.white [0] / 0.00002))); + sk_hdr_png::SetUint32 (mdcv_data.white_point.y, + static_cast (round (colorimetry.white [1] / 0.00002))); + + Chunk mdcv_chunk = {sizeof(mdcv_data), {'m','D','C','v'}, &mdcv_data}; + mdcv_chunk.write(fPNG); + } +#endif + + // Write the remainder of the original file + fwrite(insert_ptr, size - insert_pos, 1, fPNG); + fflush(fPNG); + fclose(fPNG); + + return true; + } + + return false; +} + +bool +sk_hdr_png::copy_to_clipboard (const wchar_t* image_path) +{ + std::error_code ec; + if (image_path == nullptr || !std::filesystem::exists (image_path, ec)) + { + return false; + } + + int clpSize = sizeof (DROPFILES); + + clpSize += sizeof(wchar_t) * static_cast(wcslen(image_path) + 1); // + 1 => '\0' + clpSize += sizeof(wchar_t); // two \0 needed at the end + + HDROP hdrop = static_cast (GlobalAlloc(GHND, clpSize)); + DROPFILES* df = static_cast (GlobalLock(hdrop)); + + if (df != nullptr) + { + df->pFiles = sizeof(DROPFILES); + df->fWide = TRUE; + + wcscpy((wchar_t*)&df[1], image_path); + + bool clipboard_open = false; + + for (auto attempts = 0; attempts < 8; ++attempts) + { + if (attempts > 0) + { + Sleep(1 << (attempts-1)); + } + + if (OpenClipboard(GetForegroundWindow())) + { + clipboard_open = true; + break; + } + } + + if (clipboard_open) + { + EmptyClipboard( ); + SetClipboardData(CF_HDROP, hdrop); + CloseClipboard( ); + GlobalUnlock( hdrop); + + return true; + } + + GlobalUnlock(hdrop); + } + + return false; +} + +bool +sk_hdr_png::write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard)//, reshade::api::display* display) +{ + using namespace DirectX; + using namespace DirectX::PackedVector; + + // PNG only supports 8-bpc and 16-bpc pixels; the bpc refers to the size of the pixel during encode/decode. + // + // * We have a 3-channel RGB image, thus 48-bpp when decoded. + // + // Space savings are possible by quantizing to alternate bit depths before encoding, 10-bpc is a sane minimum for HDR. + WICPixelFormatGUID wic_format = GUID_WICPixelFormat48bppRGB; + + const bool validFormat = + fmt == format::r16g16b16a16_float || + fmt == format::r10g10b10a2_unorm || + fmt == format::b10g10r10a2_unorm +#ifdef SK_HDR_PNG_COMMUNITY_SHADERS + || fmt == format::r16g16b16a16_pq +#endif + ; + + if (image_path == nullptr || width == 0 || height == 0 || !validFormat) + { + return false; + } + + // 16-byte alignment is mandatory for SIMD processing + if (pixels == nullptr || (reinterpret_cast(pixels) & 0xF) != 0) + { + return false; + } + + if (quantization_bits < 6 || quantization_bits > 16) + { + return false; + } + + com_ptr factory; + com_ptr encoder; + com_ptr bitmap_frame; + com_ptr property_bag; + com_ptr stream; + + HRESULT hr = E_OUTOFMEMORY; + + UINT row_stride = (width * 48 + 7)/8; + UINT buffer_size = height * row_stride; + + BYTE* png_buffer = (BYTE *)_aligned_malloc(sizeof ( BYTE) * buffer_size, 16); + XMFLOAT4* rgba32_scanline = (XMFLOAT4 *)_aligned_malloc(sizeof (XMFLOAT4) * width, 16); + float* luminance = (float *)_aligned_malloc(sizeof ( float) * width * height, 16); + + if (png_buffer != nullptr && rgba32_scanline != nullptr && luminance != nullptr) + { + hr = + CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, factory.put_void()); + + if (SUCCEEDED(hr)) hr = factory->CreateStream(stream.put()); + if (SUCCEEDED(hr)) hr = stream->InitializeFromFilename(image_path, GENERIC_WRITE); + if (SUCCEEDED(hr)) hr = factory->CreateEncoder(GUID_ContainerFormatPng, NULL, encoder.put()); + if (SUCCEEDED(hr)) hr = encoder->Initialize(stream.get(), WICBitmapEncoderNoCache); + if (SUCCEEDED(hr)) hr = encoder->CreateNewFrame(bitmap_frame.put(), property_bag.put()); + if (SUCCEEDED(hr)) hr = bitmap_frame->Initialize(property_bag.get()); + if (SUCCEEDED(hr)) hr = bitmap_frame->SetSize(width, height); + if (SUCCEEDED(hr)) hr = bitmap_frame->SetPixelFormat(&wic_format); + if (SUCCEEDED(hr)) hr = IsEqualGUID(wic_format, GUID_WICPixelFormat48bppRGB) ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + auto QUANTIZE_FP32_TO_UNORM16 = [](XMVECTOR& rgb32,int bit_reduce,uint16_t*& output) + { + const int quant_postscale = 1UL << bit_reduce; + const float quant_prescale = static_cast(quant_postscale); + + *(output++) = static_cast(std::min (65535, static_cast(std::roundf ((XMVectorGetX (rgb32) * quant_prescale)) * 65536.0f) / quant_postscale)); + *(output++) = static_cast(std::min (65535, static_cast(std::roundf ((XMVectorGetY (rgb32) * quant_prescale)) * 65536.0f) / quant_postscale)); + *(output++) = static_cast(std::min (65535, static_cast(std::roundf ((XMVectorGetZ (rgb32) * quant_prescale)) * 65536.0f) / quant_postscale)); + }; + + if (fmt == format::r10g10b10a2_unorm || fmt == format::b10g10r10a2_unorm) + { + uint16_t* png_pixels = (uint16_t *)png_buffer; + uint32_t* src_pixels = (uint32_t *)pixels; + + auto pixel_luminance = luminance; + + for (size_t i = 0; i < width * height; i++) + { + const uint32_t color = *reinterpret_cast(src_pixels++); + + // Multiply by 64 and +/- 1 to get 10-bit range (0-1023) into 16-bit range (0-65535) + const uint16_t c[] = { (((( color & 0x000003FFU) + 1U) * 64U) & 0xFFFFU) - 1U, + (((((color & 0x000FFC00U) >> 10U) + 1U) * 64U) & 0xFFFFU) - 1U, + (((((color & 0x3FF00000U) >> 20U) + 1U) * 64U) & 0xFFFFU) - 1U }; + + const int r = fmt == format::b10g10r10a2_unorm ? 2 : 0; + const int g = 1; + const int b = fmt == format::b10g10r10a2_unorm ? 0 : 2; + + XMVECTOR rgb = + XMVectorSet (static_cast(c [r]) / 65535.0f, + static_cast(c [g]) / 65535.0f, + static_cast(c [b]) / 65535.0f, 1.0f); + + if (quantization_bits < 10) { + QUANTIZE_FP32_TO_UNORM16 (rgb, quantization_bits, png_pixels); + } else { + quantization_bits = 10; // Cap to 10-bpc + *(png_pixels++) = c [r]; + *(png_pixels++) = c [g]; + *(png_pixels++) = c [b]; + } + + *pixel_luminance++ = + XMVectorGetY ( + XMVector3Transform ( + PQToLinear (XMVectorSaturate (rgb)), c_from2020toXYZ + ) + ); + } + + hr = bitmap_frame->WritePixels(height, row_stride, buffer_size, png_buffer); + } + + else if (fmt == format::r16g16b16a16_float) + { + uint16_t* png_pixels = (uint16_t *)png_buffer; + uint16_t* src_pixels = (uint16_t *)pixels; + + auto pixel_luminance = luminance; + + for (size_t y = 0; y < height; y++) + { + XMFLOAT4* rgba32_pixels = rgba32_scanline; + + XMConvertHalfToFloatStream ( + (float *)rgba32_pixels, sizeof (float), + src_pixels, sizeof (HALF), 4 * width + ); + + for (size_t x = 0; x < width ; x++) + { + XMVECTOR rgb = + XMLoadFloat4 (rgba32_pixels++); + + *pixel_luminance++ = + XMVectorGetY ( + XMVector3Transform (rgb, c_from709toXYZ) + ); + + rgb = + LinearToPQ ( + XMVectorMax ( + XMVector3Transform (rgb, c_from709to2020), + g_XMZero ) + ); + + if (quantization_bits < 16) + QUANTIZE_FP32_TO_UNORM16 (rgb, quantization_bits, png_pixels); + else + { + *(png_pixels++) = static_cast(std::min (65535, static_cast(XMVectorGetX (rgb) * 65536.0f))); + *(png_pixels++) = static_cast(std::min (65535, static_cast(XMVectorGetY (rgb) * 65536.0f))); + *(png_pixels++) = static_cast(std::min (65535, static_cast(XMVectorGetZ (rgb) * 65536.0f))); + } + + src_pixels += 4; + } + } + + hr = bitmap_frame->WritePixels(height, row_stride, buffer_size, png_buffer); + } + +#ifdef SK_HDR_PNG_COMMUNITY_SHADERS + else if (fmt == format::r16g16b16a16_pq) + { + uint16_t* png_pixels = (uint16_t*)png_buffer; + uint16_t* src_pixels = (uint16_t*)pixels; + + auto pixel_luminance = luminance; + + for (size_t y = 0; y < height; y++) + { + XMFLOAT4* rgba32_pixels = rgba32_scanline; + + XMConvertHalfToFloatStream( + (float*)rgba32_pixels, sizeof(float), + src_pixels, sizeof(HALF), 4 * width); + + for (size_t x = 0; x < width; x++) + { + XMVECTOR rgb = XMVectorSaturate(XMLoadFloat4(rgba32_pixels++)); + + *pixel_luminance++ = + XMVectorGetY( + XMVector3Transform( + PQToLinear(rgb), c_from2020toXYZ)); + + if (quantization_bits < 16) + QUANTIZE_FP32_TO_UNORM16(rgb, quantization_bits, png_pixels); + else + { + *(png_pixels++) = static_cast(std::min(65535, static_cast(XMVectorGetX(rgb) * 65536.0f))); + *(png_pixels++) = static_cast(std::min(65535, static_cast(XMVectorGetY(rgb) * 65536.0f))); + *(png_pixels++) = static_cast(std::min(65535, static_cast(XMVectorGetZ(rgb) * 65536.0f))); + } + + src_pixels += 4; + } + } + + hr = bitmap_frame->WritePixels(height, row_stride, buffer_size, png_buffer); + } +#endif + } + + else + { + hr = E_OUTOFMEMORY; + } + } + + if (SUCCEEDED(hr)) hr = bitmap_frame->Commit(); + if (SUCCEEDED(hr)) hr = encoder->Commit(); + if (SUCCEEDED(hr)) + { + hr = write_hdr_chunks(image_path, width, height, luminance, quantization_bits/*, display*/) ? S_OK : E_FAIL; + + if (SUCCEEDED(hr)) + { + if (use_clipboard) + hr = copy_to_clipboard(image_path) ? S_OK : E_FAIL; + } + } + + _aligned_free(png_buffer); + _aligned_free(rgba32_scanline); + _aligned_free(luminance); + + return SUCCEEDED(hr); +} + +#pragma pop_macro("_XM_F16C_INTRINSICS_") diff --git a/features/Screenshot/Shaders/Features/Screenshot.ini b/features/Screenshot/Shaders/Features/Screenshot.ini index 000b60a568..5dd39c9cbd 100644 --- a/features/Screenshot/Shaders/Features/Screenshot.ini +++ b/features/Screenshot/Shaders/Features/Screenshot.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-0-0 +Version = 1-1-0 diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index 8f6d445293..a9d3c4d347 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -4,18 +4,20 @@ // capture does not stall the frame. #include "Features/ScreenshotFeature.h" + +#include + #include "Features/HDRDisplay.h" -#include "Globals.h" +#include "Features/Upscaling.h" #include "Menu.h" #include "Utils/FileSystem.h" + #include -#include -#include -#include -#include +#include + #include -#include -#include +#include +#include namespace { @@ -141,9 +143,7 @@ namespace } } - // Tonemaps an FP16 linear scene-referred ScratchImage in-place: Reinhard - // c / (1 + c) for the luminance map, then gamma-2.2 for sRGB encoding. - // Approximates HDRDisplay's on-screen tonemap closely enough for SDR save. + // Tonemaps a linear RGB ScratchImage in-place: Reinhard c/(1+c), then gamma-2.2. void TonemapHdrToSrgb(DirectX::ScratchImage& image) { using namespace DirectX; @@ -156,7 +156,6 @@ namespace const XMVECTOR one = XMVectorSplatOne(); const XMVECTOR invGamma = XMVectorReplicate(1.0f / 2.2f); for (size_t i = 0; i < width; ++i) { - // Clamp negatives - some shaders emit tiny sub-zero values pow() would NaN on. XMVECTOR c = XMVectorMax(inPixels[i], XMVectorZero()); const XMVECTOR rgb = XMVectorDivide(c, XMVectorAdd(c, one)); const XMVECTOR gammaCorrected = XMVectorPow(rgb, invGamma); @@ -171,8 +170,6 @@ namespace const DirectX::Image* PrepareBmpImage(DirectX::ScratchImage& sourceImage, DirectX::ScratchImage& convertedImage) { - // FP16 sources carry HDR scene-referred values (peak >> 1.0) that BMP - // can't represent. Tonemap + gamma-encode before the 8-bit conversion. if (sourceImage.GetMetadata().format == DXGI_FORMAT_R16G16B16A16_FLOAT) { TonemapHdrToSrgb(sourceImage); } @@ -191,6 +188,98 @@ namespace return sourceImage.GetImage(0, 0, 0); } + // Game-root-relative paths (e.g. "Screenshots") must be absolute for CF_HDROP / Discord. + std::filesystem::path ResolveToAbsoluteGamePath(const std::filesystem::path& path) + { + if (path.is_absolute()) { + return path; + } + wchar_t buffer[MAX_PATH]{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer, MAX_PATH); + if (length > 0 && length < MAX_PATH) { + return std::filesystem::path(buffer).parent_path() / path; + } + std::error_code ec; + return std::filesystem::absolute(path, ec); + } + + bool CopyFilePathToClipboardHDrop(const std::wstring& absolutePath) + { + if (absolutePath.empty()) { + return false; + } + + const size_t pathChars = absolutePath.size(); + const size_t bytes = sizeof(DROPFILES) + (pathChars + 2) * sizeof(wchar_t); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes); + if (!hMem) { + return false; + } + + auto* drop = static_cast(GlobalLock(hMem)); + if (!drop) { + GlobalFree(hMem); + return false; + } + + drop->pFiles = sizeof(DROPFILES); + drop->fWide = TRUE; + + auto* files = reinterpret_cast(reinterpret_cast(drop) + sizeof(DROPFILES)); + memcpy(files, absolutePath.c_str(), (pathChars + 1) * sizeof(wchar_t)); + + GlobalUnlock(hMem); + + for (int attempt = 0; attempt < 8; ++attempt) { + if (attempt > 0) { + Sleep(1 << (attempt - 1)); + } + if (!OpenClipboard(nullptr)) { + continue; + } + EmptyClipboard(); + const bool placed = SetClipboardData(CF_HDROP, hMem) != nullptr; + CloseClipboard(); + if (placed) { + return true; + } + } + + GlobalFree(hMem); + return false; + } + + void RunOnMainThread(std::function fn) + { + if (auto* taskInterface = SKSE::GetTaskInterface()) { + taskInterface->AddTask(std::move(fn)); + } else { + fn(); + } + } + + void CopySavedPathToClipboard(bool enabled, const std::filesystem::path& path) + { + if (!enabled || path.empty()) { + return; + } + + const auto absolutePath = ResolveToAbsoluteGamePath(path); + std::error_code ec; + if (!std::filesystem::exists(absolutePath, ec)) { + logger::warn("Screenshot not found for clipboard: {}", absolutePath.string()); + return; + } + if (std::filesystem::file_size(absolutePath, ec) == 0) { + logger::warn("Screenshot file is empty, skipping clipboard: {}", absolutePath.string()); + return; + } + + if (!CopyFilePathToClipboardHDrop(absolutePath.wstring())) { + logger::warn("Screenshot saved but clipboard copy failed."); + } + } + // Resolves the slot's underlying texture, falling back to QueryInterface on // SRV/RTV when slot.texture is null (kFRAMEBUFFER on flat aliases the swap- // chain backbuffer that way). `holder` keeps the QI refcount alive across @@ -222,14 +311,40 @@ namespace return resolveFromView(slot.RTV); } - // Picks the capture source by where ISHDR wrote the scene this frame: - // VR -> RE::RENDER_TARGETS::kVR_FRAMEBUFFER (SBS). - // HDR enabled -> HDR::HdrTexture (FP16 linear; PrepareBmpImage tonemaps). - // otherwise -> kFRAMEBUFFER (already tonemapped UNORM). - // - // HDR::OutputTexture is intentionally not used: on HDR10 swap chains it - // holds PQ-encoded values regardless of the enableHDR toggle, which save - // as washed-out BMPs without a color transform. + // Returns the texture that was presented to the display (post-ApplyHDR). + ID3D11Texture2D* ResolveDisplayedBackBuffer(winrt::com_ptr& holder) + { + auto& upscaling = globals::features::upscaling; + if (upscaling.d3d12SwapChainActive && + upscaling.dx12SwapChain.swapChainBufferWrapped && + upscaling.dx12SwapChain.swapChainBufferWrapped->resource11) { + holder.copy_from(upscaling.dx12SwapChain.swapChainBufferWrapped->resource11); + return holder.get(); + } + + if (!globals::d3d::swapChain) { + return nullptr; + } + + winrt::com_ptr backBuffer; + if (FAILED(globals::d3d::swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), backBuffer.put_void()))) { + return nullptr; + } + holder = std::move(backBuffer); + return holder.get(); + } + + bool IsFlatHdrScreenshotCapture() + { + return !globals::game::isVR && + globals::features::hdrDisplay.loaded && + globals::features::hdrDisplay.settings.enableHDR; + } + + // Picks the capture source: + // VR -> kVR_FRAMEBUFFER (SBS). + // HDR enabled -> swap-chain back buffer after ApplyHDR (PQ HDR10 / PQ float). + // otherwise -> kFRAMEBUFFER (tonemapped UNORM). CaptureSource SelectCaptureSource(winrt::com_ptr& holder) { CaptureSource src; @@ -246,11 +361,10 @@ namespace return src; } - auto& hdr = globals::features::hdrDisplay; - if (hdr.loaded && hdr.settings.enableHDR && hdr.hdrTexture && hdr.hdrTexture->resource) { - src.texture = hdr.hdrTexture->resource.get(); - src.srv = hdr.hdrTexture->srv.get(); - src.description = "HDR::HdrTexture (FP16 linear, will tonemap)"; + if (IsFlatHdrScreenshotCapture()) { + src.texture = ResolveDisplayedBackBuffer(holder); + src.needsPreviewCache = true; + src.description = "Swap chain back buffer (HDR display composite)"; return src; } @@ -302,16 +416,139 @@ namespace } } - std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath) + std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath, bool usePng) { SYSTEMTIME st; GetLocalTime(&st); char buf[80]; - snprintf(buf, sizeof(buf), "CS_%04d-%02d-%02d_%02d-%02d-%02d_%03d.bmp", + const char* extension = usePng ? ".png" : ".bmp"; + snprintf(buf, sizeof(buf), "CS_%04d-%02d-%02d_%02d-%02d-%02d_%03d%s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, - st.wMilliseconds); - return std::filesystem::path(screenshotPath) / buf; + st.wMilliseconds, + extension); + return ResolveToAbsoluteGamePath(std::filesystem::path(screenshotPath) / buf); + } + + struct HdrFormatInfo + { + DXGI_FORMAT dxgi; + sk_hdr_png::format png; + size_t bytesPerPixel; + }; + + constexpr HdrFormatInfo kHdrFormats[] = { + { DXGI_FORMAT_R10G10B10A2_UNORM, sk_hdr_png::format::r10g10b10a2_unorm, 4 }, + { DXGI_FORMAT_R16G16B16A16_FLOAT, sk_hdr_png::format::r16g16b16a16_pq, 8 }, + }; + + const HdrFormatInfo* LookupHdrFormat(DXGI_FORMAT format) + { + for (const auto& info : kHdrFormats) { + if (info.dxgi == format) { + return &info; + } + } + return nullptr; + } + + bool IsHdrCaptureFormat(DXGI_FORMAT format) + { + return LookupHdrFormat(format) != nullptr; + } + + // sk_hdr_png requires 16-byte aligned pixel memory. + bool CopyToAlignedPixelBuffer( + const DirectX::Image& image, + size_t bytesPerPixel, + void*& outAligned, + size_t& outByteSize) + { + if (bytesPerPixel == 0) { + return false; + } + + const size_t tightRowBytes = static_cast(image.width) * bytesPerPixel; + outByteSize = tightRowBytes * image.height; + + outAligned = _aligned_malloc(outByteSize, 16); + if (!outAligned) { + return false; + } + + auto* dest = static_cast(outAligned); + const auto* src = image.pixels; + for (size_t row = 0; row < image.height; ++row) { + memcpy(dest + row * tightRowBytes, src + row * image.rowPitch, tightRowBytes); + } + return true; + } + + bool SaveHdrPng( + const DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + int quantizationBits, + DXGI_FORMAT format) + { + const DirectX::Image* firstImage = image.GetImage(0, 0, 0); + const HdrFormatInfo* hdrInfo = firstImage ? LookupHdrFormat(format) : nullptr; + if (!firstImage || !hdrInfo || firstImage->format != format) { + return false; + } + + void* alignedPixels = nullptr; + size_t byteSize = 0; + if (!CopyToAlignedPixelBuffer(*firstImage, hdrInfo->bytesPerPixel, alignedPixels, byteSize)) { + return false; + } + + const bool saved = sk_hdr_png::write_image_to_disk( + outputPath.wstring().c_str(), + static_cast(firstImage->width), + static_cast(firstImage->height), + alignedPixels, + quantizationBits, + hdrInfo->png, + false); + + _aligned_free(alignedPixels); + return saved; + } + + bool SaveSdrScreenshot( + DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + bool saveAsPng) + { + StripAlphaForBmp(image); + DirectX::ScratchImage convertedImage; + const DirectX::Image* saveImage = PrepareBmpImage(image, convertedImage); + if (!saveImage) { + return false; + } + + const GUID& codec = saveAsPng ? + DirectX::GetWICCodec(DirectX::WIC_CODEC_PNG) : + DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP); + return SUCCEEDED(DirectX::SaveToWICFile( + *saveImage, + DirectX::WIC_FLAGS_NONE, + codec, + outputPath.c_str())); + } + + bool SaveScreenshotToDisk( + DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + DXGI_FORMAT format, + int hdrPngBitDepth, + bool saveAsHdrPng, + bool saveAsSdrPng) + { + if (saveAsHdrPng) { + return SaveHdrPng(image, outputPath, hdrPngBitDepth, format); + } + return SaveSdrScreenshot(image, outputPath, saveAsSdrPng); } } @@ -347,6 +584,12 @@ void ScreenshotFeature::LoadSettings(json& a_json) screenshotPath = a_json["ScreenshotPath"]; if (a_json.contains("ApplyCropToScreenshot")) applyCropToScreenshot = a_json["ApplyCropToScreenshot"]; + if (a_json.contains("HdrPngBitDepth")) + hdrPngBitDepth = std::clamp(a_json["HdrPngBitDepth"], 7u, 16u); + if (a_json.contains("SdrUsePng")) + sdrUsePng = a_json["SdrUsePng"]; + if (a_json.contains("CopyToClipboard")) + copyToClipboard = a_json["CopyToClipboard"]; subrect.LoadSettings(a_json); } @@ -355,25 +598,64 @@ void ScreenshotFeature::SaveSettings(json& a_json) { a_json["ScreenshotPath"] = screenshotPath; a_json["ApplyCropToScreenshot"] = applyCropToScreenshot; + a_json["HdrPngBitDepth"] = hdrPngBitDepth; + a_json["SdrUsePng"] = sdrUsePng; + a_json["CopyToClipboard"] = copyToClipboard; subrect.SaveSettings(a_json); } void ScreenshotFeature::DrawSettings() { - Util::Text::Disabled("Capture and save run asynchronously - no frame stall."); - Util::Text::Disabled( - "Saves SDR .bmp files. HDR scenes are tonemapped (Reinhard) so the saved\n" - "image matches what's on screen. For true HDR files with HDR10 metadata,\n" - "use Xbox Game Bar (Win+G) or your GPU vendor's overlay (saves .jxr)."); + ImGui::TextWrapped("Capture and save run asynchronously without stalling the game."); + + const bool hdrCaptureAvailable = globals::features::hdrDisplay.loaded && + globals::features::hdrDisplay.settings.enableHDR; + + if (hdrCaptureAvailable) { + ImGui::TextWrapped( + "HDR enabled: saves the displayed frame as PNG with HDR10 metadata (48 bpp RGB, cICP/cLLi). " + "Use an HDR-aware viewer such as Windows Photos (HDR on) or Special K SKIF."); + ImGui::SliderInt( + "HDR PNG bit depth", + reinterpret_cast(&hdrPngBitDepth), + 7, + 16, + "%d-bit", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + "Quantization for the 48 bpp RGB PNG payload. 11-bit is a good default; " + "higher values increase file size with diminishing returns."); + + } else { + ImGui::TextWrapped( + "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. " + "SDR and VR captures use the lossless format selected below."); + } if (ImGui::Button("Take Screenshot Now")) { - Capture(); + captureRequested = true; } ImGui::SameLine(); ImGui::Checkbox("Apply crop", &applyCropToScreenshot); ImGui::SeparatorText("Output"); + ImGui::Checkbox("Copy saved file to clipboard", ©ToClipboard); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Places the saved screenshot on the clipboard as a file (paste in Explorer or attach in chat apps)."); + + if (!hdrCaptureAvailable || globals::game::isVR) { + int sdrFormat = sdrUsePng ? 1 : 0; + ImGui::RadioButton("BMP (lossless)", &sdrFormat, 0); + ImGui::SameLine(); + ImGui::RadioButton("PNG (lossless)", &sdrFormat, 1); + sdrUsePng = sdrFormat != 0; + if (hdrCaptureAvailable && globals::game::isVR) { + ImGui::TextWrapped("VR captures use this format. Flat HDR mode always saves HDR PNG."); + } + } + char buf[260]; strncpy_s(buf, sizeof(buf), screenshotPath.c_str(), _TRUNCATE); ImGui::PushItemWidth(-FLT_MIN - 120.0f); // leave room for Open button + label @@ -392,10 +674,9 @@ void ScreenshotFeature::DrawSettings() ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Text("Folder"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip( - "Relative paths resolve against the Skyrim install dir.\n" - "Absolute paths (e.g. D:\\Captures) save there directly."); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Relative paths resolve against the Skyrim install dir."); + ImGui::Text("Absolute paths (e.g. D:\\Captures) save there directly."); } auto& menuSettings = Menu::GetSingleton()->GetSettings(); @@ -406,10 +687,9 @@ void ScreenshotFeature::DrawSettings() "Change##ScreenshotFeature"); if (HotkeyCollidesWithVanilla()) { - Util::Text::Disabled( - "This hotkey collides with vanilla PrintScreen; both saves will fire.\n" - "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a\n" - "different hotkey above."); + Util::Text::WrappedWarning( + "This hotkey collides with vanilla PrintScreen; both saves will fire. " + "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a different hotkey above."); } ImGui::SeparatorText("Crop"); @@ -476,6 +756,10 @@ void ScreenshotFeature::EnsurePreviewCache(ID3D11Texture2D* sourceTexture) } void ScreenshotFeature::Reset() +{ +} + +void ScreenshotFeature::ProcessCaptureRequest() { if (captureRequested.exchange(false)) { Capture(); @@ -546,24 +830,26 @@ void ScreenshotFeature::ScreenshotWorkerLoop() continue; } - StripAlphaForBmp(image); - DirectX::ScratchImage convertedImage; - const DirectX::Image* saveImage = PrepareBmpImage(image, convertedImage); - if (!saveImage) { - logger::error("Failed to prepare screenshot image for BMP output."); - continue; - } - Util::FileHelpers::EnsureDirectoryExists(screenshot.outputPath.parent_path()); - HRESULT hr = DirectX::SaveToWICFile( - *saveImage, - DirectX::WIC_FLAGS_NONE, - DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP), - screenshot.outputPath.c_str()); + const bool saveOk = SaveScreenshotToDisk( + image, + screenshot.outputPath, + screenshot.format, + screenshot.hdrPngBitDepth, + screenshot.saveAsHdrPng, + screenshot.saveAsSdrPng); + if (!saveOk) { + logger::error( + "Failed to save {} screenshot.", + screenshot.saveAsHdrPng ? "HDR PNG" : "SDR"); + } + + if (saveOk) { + CopySavedPathToClipboard(screenshot.copyToClipboard, screenshot.outputPath); + } - if (FAILED(hr)) { - logger::error("Failed to save screenshot: {:x}", static_cast(hr)); + if (!saveOk) { ShowInGameNotification("Screenshot failed - see CommunityShaders.log"); } else { logger::info("Saved screenshot to {}", screenshot.outputPath.string()); @@ -576,13 +862,10 @@ void ScreenshotFeature::ScreenshotWorkerLoop() void ScreenshotFeature::ShowInGameNotification(std::string message) { - // ShowHUDMessage must run on the game's main thread; marshall via SKSE's - // task interface. Third arg dedupes spam-clicks - one toast at a time. - if (auto* taskInterface = SKSE::GetTaskInterface()) { - taskInterface->AddTask([msg = std::move(message)]() { - RE::SendHUDMessage::ShowHUDMessage(msg.c_str(), nullptr, true); - }); - } + // ShowHUDMessage must run on the game's main thread. Third arg dedupes spam-clicks. + RunOnMainThread([msg = std::move(message)]() { + RE::SendHUDMessage::ShowHUDMessage(msg.c_str(), nullptr, true); + }); } void ScreenshotFeature::Capture() @@ -647,12 +930,26 @@ void ScreenshotFeature::Capture() context->CopySubresourceRegion(stagingTexture.get(), 0, 0, 0, 0, sourceTexture, 0, &sourceRegion); + // Match SelectCaptureSource: only the flat HDR back-buffer path uses HDR PNG. + // Do not key off DXGI format alone — kFRAMEBUFFER can be float/HDR-sized in SDR mode. + const bool flatHdrCapture = IsFlatHdrScreenshotCapture(); + if (flatHdrCapture && !IsHdrCaptureFormat(srcDesc.Format)) { + logger::error("Unsupported HDR screenshot format: {}", static_cast(srcDesc.Format)); + return; + } + const bool saveAsHdrPng = flatHdrCapture && IsHdrCaptureFormat(srcDesc.Format); + const bool saveAsSdrPng = !saveAsHdrPng && sdrUsePng; + EnsureWorkerThread(); PendingScreenshot screenshot; screenshot.stagingTexture = std::move(stagingTexture); screenshot.format = srcDesc.Format; screenshot.width = copyW; screenshot.height = copyH; - screenshot.outputPath = BuildScreenshotPath(screenshotPath); + screenshot.saveAsHdrPng = saveAsHdrPng; + screenshot.saveAsSdrPng = saveAsSdrPng; + screenshot.hdrPngBitDepth = static_cast(hdrPngBitDepth); + screenshot.outputPath = BuildScreenshotPath(screenshotPath, saveAsHdrPng || saveAsSdrPng); + screenshot.copyToClipboard = copyToClipboard; EnqueueScreenshot(std::move(screenshot)); } diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index d472084207..660ed97ad4 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -27,10 +27,18 @@ struct ScreenshotFeature : public Feature virtual void PostPostLoad() override; void Capture(); + // Runs after HDR Present processing so the back buffer matches what's on screen. + void ProcessCaptureRequest(); bool applyCropToScreenshot = true; // Settings std::string screenshotPath = "Screenshots"; + // HDR PNG quantization (7-16); used when HDR Display captures the back buffer. + unsigned int hdrPngBitDepth = 11; + // SDR / VR output (HDR captures always use PNG). + bool sdrUsePng = false; + // After save, put the file path on the clipboard (CF_HDROP). + bool copyToClipboard = false; std::atomic captureRequested{ false }; @@ -42,6 +50,10 @@ struct ScreenshotFeature : public Feature uint32_t width = 0; uint32_t height = 0; std::filesystem::path outputPath; + bool saveAsHdrPng = false; + bool saveAsSdrPng = false; + int hdrPngBitDepth = 11; + bool copyToClipboard = false; }; std::mutex screenshotQueueMutex; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index afe2d4cda7..4c8345a51b 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -12,6 +12,7 @@ #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" +#include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" #include "Features/Upscaling.h" #include "Features/VR.h" @@ -244,6 +245,8 @@ struct IDXGISwapChain_Present return func(swapChain, syncInterval, presentFlags); }); + globals::features::screenshotFeature.ProcessCaptureRequest(); + TracyD3D11Collect(globals::state->tracyCtx); return retval; From f62f4b1bba203b494cf73bb90c864634e6925dd6 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Sun, 31 May 2026 14:07:26 +0200 Subject: [PATCH 03/55] refactor(weather-editor): migrate isl light editor (#2414) Co-authored-by: Claude Sonnet 4.6 --- src/Features/InverseSquareLighting.cpp | 16 ++------ src/Features/InverseSquareLighting.h | 10 +---- src/WeatherEditor/EditorWindow.cpp | 18 ++++++++- src/WeatherEditor/EditorWindow.h | 3 ++ .../LightEditor.cpp | 40 +++++++------------ .../LightEditor.h | 10 ++--- 6 files changed, 45 insertions(+), 52 deletions(-) rename src/{Features/InverseSquareLighting => WeatherEditor}/LightEditor.cpp (96%) rename src/{Features/InverseSquareLighting => WeatherEditor}/LightEditor.h (94%) diff --git a/src/Features/InverseSquareLighting.cpp b/src/Features/InverseSquareLighting.cpp index a602079470..03e830456d 100644 --- a/src/Features/InverseSquareLighting.cpp +++ b/src/Features/InverseSquareLighting.cpp @@ -1,18 +1,9 @@ #include "InverseSquareLighting.h" #include "Features/InverseSquareLighting/Common.h" #include "LightLimitFix.h" +#include "WeatherEditor/EditorWindow.h" #include -void InverseSquareLighting::DrawSettings() -{ - editor.DrawSettings(); -} - -void InverseSquareLighting::EarlyPrepass() -{ - editor.GatherLights(); -} - void InverseSquareLighting::PostPostLoad() { stl::detour_thunk(REL::RelocationID(17208, 17610)); @@ -55,13 +46,14 @@ void InverseSquareLighting::ProcessLight(LightLimitFix::LightData& light, RE::BS runtimeData->flags.set(LightLimitFix::LightFlags::Initialised); } - editor.ApplyOverrides(niLight, runtimeData); + const auto& editorRef = EditorWindow::GetSingleton()->lightEditor; + editorRef.ApplyOverrides(niLight, runtimeData); light.lightFlags = runtimeData->flags; light.color = { runtimeData->diffuse.red, runtimeData->diffuse.green, runtimeData->diffuse.blue }; const bool isInvSq = light.lightFlags.any(LightLimitFix::LightFlags::InverseSquare); - if (bsLight->pointLight && editor.enabled && ((isInvSq && editor.disableInvSqLights) || (!isInvSq && editor.disableRegularLights))) + if (bsLight->pointLight && ((isInvSq && editorRef.disableInvSqLights) || (!isInvSq && editorRef.disableRegularLights))) light.lightFlags.set(LightLimitFix::LightFlags::Disabled); if (bsLight->pointLight && isInvSq) { diff --git a/src/Features/InverseSquareLighting.h b/src/Features/InverseSquareLighting.h index 70699c3ac3..fdf65676d0 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -1,5 +1,4 @@ #pragma once -#include "Features/InverseSquareLighting/LightEditor.h" #include "LightLimitFix.h" struct InverseSquareLighting : Feature @@ -21,17 +20,12 @@ struct InverseSquareLighting : Feature "Lights smoothly fade out at a configurable cutoff, solving the infinite distance problem", "Does not modify any existing lighting", "Requires the use of mods with lights enabled for inverse square falloff.", - "Full integration with Light Placer", - "Built in Light Editor for mod authors to preview lighting changes in real-time" } + "Full integration with Light Placer" } }; } inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }; - virtual void DrawSettings() override; - - virtual void EarlyPrepass() override; - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; @@ -56,8 +50,6 @@ struct InverseSquareLighting : Feature virtual bool IsCore() const override { return true; }; private: - LightEditor editor = LightEditor(); - static constexpr float DefaultCutoff = 0.05f; static constexpr float DefaultShadowCasterCutoff = 0.022f; diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 7f84372e48..9d31591f82 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -213,7 +213,9 @@ void EditorWindow::ShowObjectsWindow() ImGui::Spacing(); // List of categories - const char* categories[] = { "Weather", "ImageSpace", "Lighting Template", "Cell Lighting", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect", "Interior Only" }; + const char* categories[] = { "Weather", "ImageSpace", "Lighting Template", "Cell Lighting", + "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect", + "Interior Only", "Lighting editor" }; for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { // Highlight the selected category if (ImGui::Selectable(categories[i], m_selectedCategory == categories[i])) { @@ -238,6 +240,16 @@ void EditorWindow::ShowObjectsWindow() return; } + if (m_selectedCategory == "Lighting editor") { + BeginScrollableContent("##LightEditorScroll"); + lightEditor.DrawSettings(); + EndScrollableContent(); + ImGui::EndChild(); + ImGui::EndTable(); + ImGui::End(); + return; + } + // Returns the widget collection for a given category; Cell Lighting and unknown // categories return an empty collection since they have no standalone widget list. auto getWidgetsForCategory = [&](const std::string& cat) -> const std::vector>& { @@ -1402,6 +1414,7 @@ void EditorWindow::UpdateOpenState() BackgroundBlur::SetWeatherEditorActive(IsViewportActive()); } else if (!open && wasOpen) { + lightEditor.ResetOverrides(); RestoreVanityCamera(); ShowGameMenus(); BackgroundBlur::SetWeatherEditorActive(false); @@ -1412,6 +1425,9 @@ void EditorWindow::UpdateOpenState() void EditorWindow::Draw() { + if (open) + lightEditor.GatherLights(); + // Keep background blur in sync when HDR toggles while the editor stays open { static bool prevViewportActive = false; diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index e53c3cc8a1..266a75f961 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -10,6 +10,7 @@ #include "Weather/ReferenceEffectWidget.h" #include "Weather/VolumetricLightingWidget.h" #include "Weather/WeatherWidget.h" +#include "LightEditor.h" #include "WeatherUtils.h" #include "Widget.h" @@ -63,6 +64,8 @@ class EditorWindow // Owned by EditorWindow, created on demand in ShowObjectsWindow(), released in destructor std::unique_ptr currentCellLightingWidget; + LightEditor lightEditor; + // Weather locking for editing RE::TESWeather* lockedWeather = nullptr; bool weatherLockActive = false; diff --git a/src/Features/InverseSquareLighting/LightEditor.cpp b/src/WeatherEditor/LightEditor.cpp similarity index 96% rename from src/Features/InverseSquareLighting/LightEditor.cpp rename to src/WeatherEditor/LightEditor.cpp index abcfdb0a11..f405f53568 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/WeatherEditor/LightEditor.cpp @@ -1,7 +1,7 @@ -#include "Features/InverseSquareLighting/LightEditor.h" -#include "Features/InverseSquareLighting.h" -#include "Features/LightLimitFix.h" -#include "Menu.h" +#include "LightEditor.h" +#include "../Features/InverseSquareLighting.h" +#include "../Features/LightLimitFix.h" +#include "../Menu.h" #include #include @@ -10,21 +10,6 @@ void LightEditor::DrawSettings() { - ImGui::Checkbox("Enable Light Editor", &enabled); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Allows for modifying lights in real-time to preview changes. " - "Light Placer lights can be saved back to their JSON configs. " - "Not intended for gameplay use."); - } - - if (!enabled) - return; - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - ImGui::Checkbox("Disable Regular Falloff Lights", &disableRegularLights); ImGui::Checkbox("Disable Inverse Square Falloff Lights", &disableInvSqLights); @@ -168,10 +153,8 @@ std::string LightEditor::GetLightName(LightInfo& lightInfo) void LightEditor::GatherLights() { - if (!enabled || !Menu::GetSingleton()->ShouldSwallowInput()) { - RestoreOriginal(); - selected = {}; - previous = {}; + if (!Menu::GetSingleton()->ShouldSwallowInput()) { + ResetOverrides(); return; } @@ -287,6 +270,13 @@ void LightEditor::GatherLights() SortLights(); } +void LightEditor::ResetOverrides() +{ + RestoreOriginal(); + selected = {}; + previous = {}; +} + void LightEditor::UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH* ligh, RE::NiLight* niLight) { const auto runtimeData = ISLCommon::RuntimeLightDataExt::Get(niLight); @@ -372,7 +362,7 @@ void LightEditor::UpdateSelectedLight(RE::TESObjectREFR* refr, RE::TESObjectLIGH bool LightEditor::ApplyOverrides(RE::NiLight* niLight, ISLCommon::RuntimeLightDataExt* runtimeData) const { - if (!enabled || niLight != activeNiLight.get()) + if (niLight != activeNiLight.get()) return false; runtimeData->diffuse = current.data.diffuse; @@ -707,4 +697,4 @@ void LightEditor::SortLights() default: break; } -} \ No newline at end of file +} diff --git a/src/Features/InverseSquareLighting/LightEditor.h b/src/WeatherEditor/LightEditor.h similarity index 94% rename from src/Features/InverseSquareLighting/LightEditor.h rename to src/WeatherEditor/LightEditor.h index 1748fa34fd..48be6b33c7 100644 --- a/src/Features/InverseSquareLighting/LightEditor.h +++ b/src/WeatherEditor/LightEditor.h @@ -1,15 +1,15 @@ -#pragma once -#include "Features/InverseSquareLighting/Common.h" +#pragma once +#include "../Features/InverseSquareLighting/Common.h" struct LightEditor { - bool enabled; - bool disableInvSqLights; - bool disableRegularLights; + bool disableInvSqLights = false; + bool disableRegularLights = false; bool shadowsOnly = false; void DrawSettings(); void GatherLights(); + void ResetOverrides(); bool ApplyOverrides(RE::NiLight* niLight, ISLCommon::RuntimeLightDataExt* runtimeData) const; From aa0e1a6768dee13111efb0a2ae3f363dc93ff255 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 31 May 2026 13:12:59 +0100 Subject: [PATCH 04/55] feat: add GPU/CPU profiling system with per-feature timing (#2389) Co-authored-by: Claude Opus 4.6 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Shaders/Features/DynamicCubemaps.ini | 5 +- .../Shaders/Features/GrassCollision.ini | 5 +- .../Shaders/Features/HDRDisplay.ini | 2 +- .../Shaders/Features/ImageBasedLighting.ini | 2 +- .../Shaders/Features/LightLimitFix.ini | 5 +- .../Shaders/Features/PerformanceOverlay.ini | 5 +- .../Shaders/Features/ScreenSpaceGI.ini | 2 +- .../Shaders/Features/Skylighting.ini | 2 +- .../Shaders/Features/SubsurfaceScattering.ini | 7 +- .../Shaders/Features/TerrainBlending.ini | 2 +- .../Shaders/Features/TerrainShadows.ini | 5 +- .../Shaders/Features/VolumetricShadows.ini | 5 +- include/PCH.h | 1 + src/Deferred.cpp | 4 + src/Features/DynamicCubemaps.cpp | 8 + src/Features/GrassCollision.cpp | 3 + src/Features/HDRDisplay.cpp | 4 + src/Features/IBL.cpp | 4 + src/Features/LightLimitFix.cpp | 4 + src/Features/PerformanceOverlay.cpp | 43 +- src/Features/PerformanceOverlay.h | 4 +- src/Features/ScreenSpaceGI.cpp | 17 + src/Features/ScreenSpaceShadows.cpp | 8 + src/Features/Skylighting.cpp | 7 + src/Features/SubsurfaceScattering.cpp | 6 + src/Features/TerrainBlending.cpp | 2 + src/Features/TerrainShadows.cpp | 2 + src/Features/Upscaling.cpp | 12 + src/Features/Upscaling/RCAS/RCAS.cpp | 2 + src/Features/VRStereoOptimizations.cpp | 4 + src/Features/VolumetricShadows.cpp | 13 + src/Globals.cpp | 3 + src/Globals.h | 2 + src/Menu/FeatureListRenderer.cpp | 10 +- src/Menu/ProfilingRenderer.cpp | 408 ++++++++++++++++++ src/Menu/ProfilingRenderer.h | 68 +++ src/Profiler.cpp | 236 ++++++++++ src/Profiler.h | 146 +++++++ src/State.cpp | 12 + src/Utils/LegitProfiler.h | 292 +++++++++++++ 40 files changed, 1350 insertions(+), 22 deletions(-) create mode 100644 src/Menu/ProfilingRenderer.cpp create mode 100644 src/Menu/ProfilingRenderer.h create mode 100644 src/Profiler.cpp create mode 100644 src/Profiler.h create mode 100644 src/Utils/LegitProfiler.h diff --git a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini index bf7e80975a..c87a9215ee 100644 --- a/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini +++ b/features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini @@ -1,2 +1,5 @@ [Info] -Version = 2-3-2 +Version = 2-4-0 + +[Nexus] +autoupload = false \ No newline at end of file diff --git a/features/Grass Collision/Shaders/Features/GrassCollision.ini b/features/Grass Collision/Shaders/Features/GrassCollision.ini index e208a6c511..620cd93bd1 100644 --- a/features/Grass Collision/Shaders/Features/GrassCollision.ini +++ b/features/Grass Collision/Shaders/Features/GrassCollision.ini @@ -1,2 +1,5 @@ [Info] -Version = 3-0-5 +Version = 3-1-0 + +[Nexus] +autoupload = false \ No newline at end of file diff --git a/features/HDR Display/Shaders/Features/HDRDisplay.ini b/features/HDR Display/Shaders/Features/HDRDisplay.ini index f175aae226..284c89b1aa 100644 --- a/features/HDR Display/Shaders/Features/HDRDisplay.ini +++ b/features/HDR Display/Shaders/Features/HDRDisplay.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-0-2 +Version = 1-1-0 [Nexus] nexusmodid = 179371 diff --git a/features/IBL/Shaders/Features/ImageBasedLighting.ini b/features/IBL/Shaders/Features/ImageBasedLighting.ini index d4094c367b..9e325f8475 100644 --- a/features/IBL/Shaders/Features/ImageBasedLighting.ini +++ b/features/IBL/Shaders/Features/ImageBasedLighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-1 +Version = 1-2-0 [Nexus] autoupload = false diff --git a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini index 2eaf45d39a..620cd93bd1 100644 --- a/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini +++ b/features/Light Limit Fix/Shaders/Features/LightLimitFix.ini @@ -1,2 +1,5 @@ [Info] -Version = 3-0-4 +Version = 3-1-0 + +[Nexus] +autoupload = false \ No newline at end of file diff --git a/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini b/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini index 5dd39c9cbd..9e325f8475 100644 --- a/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini +++ b/features/Performance Overlay/Shaders/Features/PerformanceOverlay.ini @@ -1,2 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 + +[Nexus] +autoupload = false diff --git a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini index 6c38cc792a..4791815a72 100644 --- a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini +++ b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini @@ -1,5 +1,5 @@ [Info] -Version = 4-1-1 +Version = 4-2-0 [Nexus] nexusmodid = 130375 diff --git a/features/Skylighting/Shaders/Features/Skylighting.ini b/features/Skylighting/Shaders/Features/Skylighting.ini index 3d321098b2..9bc5714af3 100644 --- a/features/Skylighting/Shaders/Features/Skylighting.ini +++ b/features/Skylighting/Shaders/Features/Skylighting.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-3-0 +Version = 1-4-0 [Nexus] nexusmodid = 139352 diff --git a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini index d38552c323..cddff0b2bc 100644 --- a/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini +++ b/features/Subsurface Scattering/Shaders/Features/SubsurfaceScattering.ini @@ -1,2 +1,7 @@ [Info] -Version = 3-0-2 +Version = 3-1-0 + +[Nexus] +nexusmodid = 114114 +nexusfilename = Subsurface Scattering +autoupload = true \ No newline at end of file diff --git a/features/Terrain Blending/Shaders/Features/TerrainBlending.ini b/features/Terrain Blending/Shaders/Features/TerrainBlending.ini index dbd4e4f58e..160714a98d 100644 --- a/features/Terrain Blending/Shaders/Features/TerrainBlending.ini +++ b/features/Terrain Blending/Shaders/Features/TerrainBlending.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 [Nexus] nexusmodid = 157076 diff --git a/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini b/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini index 5dd39c9cbd..b80d1b6ac4 100644 --- a/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini +++ b/features/Terrain Shadows/Shaders/Features/TerrainShadows.ini @@ -1,2 +1,5 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 + +[Nexus] +autoupload = false \ No newline at end of file diff --git a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini index 629d28c0f7..0aa174da38 100644 --- a/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini +++ b/features/Volumetric Shadows/Shaders/Features/VolumetricShadows.ini @@ -1,2 +1,5 @@ [Info] -Version = 2-0-2 +Version = 2-1-0 + +[Nexus] +autoupload = false \ No newline at end of file diff --git a/include/PCH.h b/include/PCH.h index e3a8d9461f..565fc4e157 100644 --- a/include/PCH.h +++ b/include/PCH.h @@ -205,6 +205,7 @@ using float4x4 = DirectX::SimpleMath::Matrix; using uint = uint32_t; #include "Globals.h" +#include "Profiler.h" #include "Util.h" #include "Feature.h" #include "Buffer.h" diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 870d1354e3..cecfb90483 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -391,7 +391,9 @@ void Deferred::DeferredPasses() { TracyD3D11Zone(globals::state->tracyCtx, "Deferred Composite - Dispatch"); + globals::profiler->BeginPass("DeferredComposite"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } // Unbind mode texture SRV @@ -411,7 +413,9 @@ void Deferred::DeferredPasses() // VR: Stereo reprojection fills Eye 1 holes here (after DeferredComposite, before SSR/water/sky) // so that ISReflectionsRayTracing sees valid pixels in both eyes. if (globals::game::isVR) { + globals::profiler->BeginPass("VR::StereoBlend"); globals::features::vr.DrawStereoBlend(); + globals::profiler->EndPass(); } // Clear diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 560c902452..5fe922e753 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -374,7 +374,9 @@ void DynamicCubemaps::UpdateCubemapCapture(bool a_reflections) context->CSSetShader(a_reflections ? (fakeReflections ? GetComputeShaderUpdateFakeReflections() : GetComputeShaderUpdateReflections()) : GetComputeShaderUpdate(), nullptr, 0); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::CaptureReflections" : "DynamicCubemaps::Capture"); context->Dispatch((uint32_t)std::ceil(envCaptureTexture->desc.Width / 8.0f), (uint32_t)std::ceil(envCaptureTexture->desc.Height / 8.0f), 6); + globals::profiler->EndPass(); uavs[0] = nullptr; uavs[1] = nullptr; @@ -415,7 +417,9 @@ void DynamicCubemaps::Inferrence(bool a_reflections) context->CSSetShader(a_reflections ? (fakeReflections ? GetComputeShaderInferrenceFakeReflections() : GetComputeShaderInferrenceReflections()) : GetComputeShaderInferrence(), nullptr, 0); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::InferReflections" : "DynamicCubemaps::Infer"); context->Dispatch((uint32_t)std::ceil(envCaptureTexture->desc.Width / 8.0f), (uint32_t)std::ceil(envCaptureTexture->desc.Height / 8.0f), 6); + globals::profiler->EndPass(); srvs[0] = nullptr; srvs[1] = nullptr; @@ -458,6 +462,7 @@ void DynamicCubemaps::Irradiance(bool a_reflections) std::uint32_t size = std::max(envTexture->desc.Width, envTexture->desc.Height) / 2; + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::IrradianceReflections" : "DynamicCubemaps::Irradiance"); for (std::uint32_t level = 1; level < MIPLEVELS; level++, size /= 2) { const UINT numGroups = (UINT)std::max(1u, size / 8); @@ -469,6 +474,7 @@ void DynamicCubemaps::Irradiance(bool a_reflections) context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); context->Dispatch(numGroups, numGroups, 6); } + globals::profiler->EndPass(); } ID3D11ShaderResourceView* nullSRV = { nullptr }; @@ -503,6 +509,7 @@ void DynamicCubemaps::CompressToBC6H(bool a_reflections) std::uint32_t mipDim = std::max(envTexture->desc.Width, envTexture->desc.Height); + globals::profiler->BeginPass(a_reflections ? "DynamicCubemaps::BC6HReflections" : "DynamicCubemaps::BC6H"); for (std::uint32_t level = 0; level < bc6hMipLevels; ++level) { std::uint32_t srcWidth = std::max(1u, mipDim >> level); std::uint32_t srcHeight = std::max(1u, mipDim >> level); @@ -521,6 +528,7 @@ void DynamicCubemaps::CompressToBC6H(bool a_reflections) std::uint32_t dispatchY = std::max(1u, (blocksY + 7) / 8); context->Dispatch(dispatchX, dispatchY, 6); } + globals::profiler->EndPass(); { ID3D11ShaderResourceView* nullSRV = nullptr; diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index 3dacd62da4..6f0feae96c 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -1,5 +1,6 @@ #include "GrassCollision.h" +#include "Globals.h" #include "State.h" #include "Utils/ActorUtils.h" #include "Utils/D3D.h" @@ -396,7 +397,9 @@ void GrassCollision::UpdateCollisionTexture() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); context->CSSetShader(GetCollisionUpdateCS(), nullptr, 0); + globals::profiler->BeginPass("GrassCollision::CollisionUpdate"); context->Dispatch(512 / 8, 512 / 8, 1); + globals::profiler->EndPass(); } context->CSSetShader(nullptr, nullptr, 0); diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 356e3f5aa7..1e01f66e2a 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -1244,7 +1244,9 @@ void HDRDisplay::ApplyHDR() } context->CSSetShader(computeShader, nullptr, 0); + globals::profiler->BeginPass("HDRDisplay::HDROutput"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); views[0] = nullptr; views[1] = nullptr; @@ -1498,7 +1500,9 @@ void HDRDisplay::ScaleUIBrightnessForFG() auto computeShader = GetUIBrightnessCS(); if (computeShader) { context->CSSetShader(computeShader, nullptr, 0); + globals::profiler->BeginPass("HDRDisplay::UIBrightness"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } // Cleanup diff --git a/src/Features/IBL.cpp b/src/Features/IBL.cpp index dfeafd7ded..f0f82a0140 100644 --- a/src/Features/IBL.cpp +++ b/src/Features/IBL.cpp @@ -226,7 +226,9 @@ void IBL::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(GetDiffuseIBLCS(), nullptr, 0); + globals::profiler->BeginPass("IBL::EnvDiffuseIBL"); context->Dispatch(1, 1, 1); + globals::profiler->EndPass(); } else { // Still need to set sampler and shader for sky IBL dispatch below context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); @@ -242,7 +244,9 @@ void IBL::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + globals::profiler->BeginPass("IBL::SkyDiffuseIBL"); context->Dispatch(1, 1, 1); + globals::profiler->EndPass(); } // Reset diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index c7528fe44c..5d8da8cc2d 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -513,7 +513,9 @@ void LightLimitFix::UpdateStructure() context->CSSetUnorderedAccessViews(0, 1, &clusters_uav, nullptr); context->CSSetShader(clusterBuildingCS, nullptr, 0); + globals::profiler->BeginPass("LightLimitFix::ClusterBuild"); context->Dispatch(clusterSize[0], clusterSize[1], clusterSize[2]); + globals::profiler->EndPass(); ID3D11UnorderedAccessView* null_uav = nullptr; context->CSSetUnorderedAccessViews(0, 1, &null_uav, nullptr); @@ -539,7 +541,9 @@ void LightLimitFix::UpdateStructure() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); context->CSSetShader(clusterCullingCS, nullptr, 0); + globals::profiler->BeginPass("LightLimitFix::ClusterCull"); context->Dispatch((clusterSize[0] + 15) / 16, (clusterSize[1] + 15) / 16, (clusterSize[2] + 3) / 4); + globals::profiler->EndPass(); } context->CSSetShader(nullptr, nullptr, 0); diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 957e591d02..739e86d8d9 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -24,6 +24,7 @@ #include "Features/Upscaling.h" #include "Globals.h" #include "Menu.h" +#include "Menu/ProfilingRenderer.h" #include "State.h" #include "Utils/FileSystem.h" #include "Utils/Format.h" @@ -103,6 +104,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ShowInOverlay, ShowDrawCalls, ShowVRAM, + ShowCSPasses, ShowFPS, ShowPreFGFrameTimeGraph, ShowPostFGFrameTimeGraph, @@ -171,6 +173,7 @@ void PerformanceOverlay::DrawSettings() ImGui::Checkbox("Show FPS Counter", &this->settings.ShowFPS); ImGui::Checkbox("Show Draw Calls", &this->settings.ShowDrawCalls); ImGui::Checkbox("Show VRAM Usage", &this->settings.ShowVRAM); + ImGui::Checkbox("Show CS Render Passes", &this->settings.ShowCSPasses); bool isFrameGenerationActive = globals::features::upscaling.IsFrameGenerationActive(); if (this->settings.ShowFPS && isFrameGenerationActive) { @@ -373,19 +376,32 @@ void PerformanceOverlay::DrawOverlay() // Update graph values this->UpdateGraphValues(); - // Show FPS counter if enabled + bool needsSeparator = false; + if (this->settings.ShowFPS) { DrawFPS(); + needsSeparator = true; } - // Show Draw Calls if enabled if (this->settings.ShowDrawCalls) { + if (needsSeparator) + ImGui::Separator(); DrawDrawCallsTable(mainRows, summaryRows); + needsSeparator = true; + } + + if (this->settings.ShowCSPasses) { + if (needsSeparator) + ImGui::Separator(); + ProfilingRenderer::RenderStatistics(false, false); + needsSeparator = true; } - // VRAM & GPU Usage if (this->settings.ShowVRAM && menu->GetDXGIAdapter3()) { + if (needsSeparator) + ImGui::Separator(); DrawVRAM(); + needsSeparator = true; } ImGui::PopStyleVar(); // ItemSpacing @@ -492,6 +508,7 @@ void PerformanceOverlay::DrawFPS() } } + void PerformanceOverlay::DrawVRAM() { auto menu = Menu::GetSingleton(); @@ -1568,6 +1585,12 @@ std::pair, std::vector> PerformanceOverlay auto [otherFrameTime, otherPercent, totalCostPerCall] = CalculateSummaryData(smoothedFrameTime, measuredSum); if (std::abs(otherFrameTime) < 1e-4f) otherFrameTime = 0.0f; + + float csPassesTime = globals::profiler->GetTotalTimeMs(); + float csPercent = smoothedFrameTime > 0.0f ? (csPassesTime / smoothedFrameTime) * 100.0f : 0.0f; + float remainingOtherTime = std::max(0.0f, otherFrameTime - csPassesTime); + float remainingOtherPercent = smoothedFrameTime > 0.0f ? (remainingOtherTime / smoothedFrameTime) * 100.0f : 0.0f; + std::optional otherTestFrameTime, otherTestCostPerCall, totalTestFrameTime, totalTestCostPerCall; auto itOther = this->testData.find(magic_enum::enum_integer(SpecialShaderType::Other)); if (itOther != this->testData.end()) { @@ -1579,15 +1602,20 @@ std::pair, std::vector> PerformanceOverlay totalTestFrameTime = itTotal->second.frameTime; totalTestCostPerCall = itTotal->second.costPerCall; } + DrawCallRow csPassesRow = { + "CS Passes:", magic_enum::enum_integer(SpecialShaderType::CSPasses), kDrawCallsNotApplicable, csPassesTime, csPercent, + 0.0f, + std::string("GPU time spent in Community Shaders compute passes (profiled)."), + true, std::nullopt, std::nullopt + }; DrawCallRow otherRow = { - "Other:", magic_enum::enum_integer(SpecialShaderType::Other), kDrawCallsNotApplicable, otherFrameTime, otherPercent, + "Other:", magic_enum::enum_integer(SpecialShaderType::Other), kDrawCallsNotApplicable, remainingOtherTime, remainingOtherPercent, 0.0f, - std::string("Frame time not attributed to any measured shader type. This includes UI, post-processing, engine work, and any GPU activity not directly measured by the overlay."), + std::string("Frame time not attributed to any measured shader type or CS compute pass. This includes UI, post-processing, engine work, and any GPU activity not directly measured."), true, otherTestFrameTime, otherTestCostPerCall }; - // Always use the actual total frame time for live data float totalFrameTime = smoothedFrameTime; - float totalPercent = 100.0f; // Total is always 100% of total + float totalPercent = 100.0f; DrawCallRow totalRow = { "Total:", magic_enum::enum_integer(SpecialShaderType::Total), static_cast(globals::state->GetTotalSmoothedDrawCalls()), totalFrameTime, totalPercent, @@ -1596,6 +1624,7 @@ std::pair, std::vector> PerformanceOverlay true, totalTestFrameTime, totalTestCostPerCall }; std::vector summaryRows; + summaryRows.push_back(csPassesRow); summaryRows.push_back(otherRow); summaryRows.push_back(totalRow); return { mainRows, summaryRows }; diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index d29fc6bebd..580cb74e14 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -16,7 +16,8 @@ struct DrawCallRow; enum class SpecialShaderType { Total = -1, - Other = -2 + Other = -2, + CSPasses = -3 }; // Constants for special draw call values @@ -267,6 +268,7 @@ struct PerformanceOverlay : OverlayFeature bool ShowInOverlay = true; // was: Enabled bool ShowDrawCalls = true; + bool ShowCSPasses = true; bool ShowVRAM = true; bool ShowFPS = true; bool ShowPreFGFrameTimeGraph = true; diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 5a021c3cd8..f40020466f 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -342,6 +342,7 @@ void ScreenSpaceGI::DrawSettings() ImGui::TreePop(); } + } void ScreenSpaceGI::LoadSettings(json& o_json) @@ -779,7 +780,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(prefilterDepthsCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterDepths"); context->Dispatch((resolution[0] + 15) >> 4, (resolution[1] + 15) >> 4, 1); + globals::profiler->EndPass(); } // fetch radiance and disocclusion @@ -809,7 +812,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(radianceDisoccCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::RadianceDisocc"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); // Prefilter radiance texture instead of using GenerateMips for proper dynamic resolution handling. // radianceDisocc wrote mip 0 directly to texRadianceTemp above, so we can bind it @@ -828,7 +833,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, 1, srvs.data()); context->CSSetUnorderedAccessViews(0, 5, uavs.data(), nullptr); context->CSSetShader(prefilterRadianceCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterRadiance"); context->Dispatch((internalRes[0] + 15u) >> 4, (internalRes[1] + 15u) >> 4, 1); + globals::profiler->EndPass(); } inputAoTexIdx = !inputAoTexIdx; @@ -851,7 +858,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, 1, srvs.data()); context->CSSetUnorderedAccessViews(0, 5, uavs.data(), nullptr); context->CSSetShader(prefilterNormalCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::PrefilterNormals"); context->Dispatch((internalRes[0] + 15u) >> 4, (internalRes[1] + 15u) >> 4, 1); + globals::profiler->EndPass(); } // GI @@ -878,7 +887,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(giCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::GI"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; @@ -904,7 +915,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(blurCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::Blur"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputGITexIdx = !inputGITexIdx; lastFrameGITexIdx = inputGITexIdx; @@ -932,7 +945,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(stereoSyncCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::StereoSync"); context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; @@ -958,7 +973,9 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(upsampleCompute.get(), nullptr, 0); + globals::profiler->BeginPass("ScreenSpaceGI::Upsample"); context->Dispatch((resolution[0] + 7u) >> 3, (resolution[1] + 7u) >> 3, 1); + globals::profiler->EndPass(); inputAoTexIdx = !inputAoTexIdx; inputGITexIdx = !inputGITexIdx; diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index e4418db016..ffc7cce408 100644 --- a/src/Features/ScreenSpaceShadows.cpp +++ b/src/Features/ScreenSpaceShadows.cpp @@ -195,6 +195,9 @@ void ScreenSpaceShadows::DrawShadows() // Shared dispatch logic for both VR and non-VR auto DispatchEye = [&](const char* eyeName, ID3D11ComputeShader* shader, const float* lightProj, float invTexSizeX, float invTexSizeY) { + std::string timerName = eyeName ? std::format("ScreenSpaceShadows::RayMarch({})", eyeName) : "ScreenSpaceShadows::RayMarch"; + globals::profiler->BeginPass(timerName); + if (globals::state->frameAnnotations && eyeName) { std::string eventName = std::format("SSS - Ray March ({})", eyeName); globals::state->BeginPerfEvent(eventName); @@ -243,6 +246,8 @@ void ScreenSpaceShadows::DrawShadows() if (globals::state->frameAnnotations) { globals::state->EndPerfEvent(); } + + globals::profiler->EndPass(); }; float InvTexSizeX = 1.0f / (float)viewportSize[0]; @@ -300,6 +305,7 @@ void ScreenSpaceShadows::DrawStereoSync() globals::state->BeginPerfEvent("SSS - Stereo Sync"); auto context = globals::d3d::context; + globals::profiler->BeginPass("ScreenSpaceShadows::StereoSync"); context->CopyResource(stereoSyncCopyTex->resource.get(), screenSpaceShadowsTexture->resource.get()); @@ -339,6 +345,8 @@ void ScreenSpaceShadows::DrawStereoSync() context->CSSetConstantBuffers(1, 1, &cbPtr); context->CSSetShader(nullptr, nullptr, 0); + globals::profiler->EndPass(); + if (globals::state->frameAnnotations) globals::state->EndPerfEvent(); } diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index 771c5240a4..3b05dd7bec 100644 --- a/src/Features/Skylighting.cpp +++ b/src/Features/Skylighting.cpp @@ -50,6 +50,7 @@ void Skylighting::DrawSettings() ImGui::SliderAngle("Max Zenith Angle", &settings.MaxZenith, 0, 90); if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Smaller angles creates more focused top-down shadow."); + } void Skylighting::SetupResources() @@ -227,7 +228,9 @@ void Skylighting::Prepass() context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); context->CSSetShader(probeUpdateCompute.get(), nullptr, 0); + globals::profiler->BeginPass("Skylighting::ProbeUpdate"); context->Dispatch((probeArrayDims[0] + 7u) >> 3, (probeArrayDims[1] + 7u) >> 3, probeArrayDims[2]); + globals::profiler->EndPass(); } // Reset @@ -513,7 +516,9 @@ void Skylighting::RenderOcclusion() auto particleShaderProperty = netimmerse_cast(shaderProp); auto rain = (RE::BSParticleShaderRainEmitter*)(particleShaderProperty->particleEmitter); + globals::profiler->BeginPass("Skylighting::PrecipMask"); precip->RenderMask(rain); + globals::profiler->EndPass(); } state->EndPerfEvent(); @@ -586,7 +591,9 @@ void Skylighting::RenderOcclusion() BSParticleShaderRainEmitter* rain = new BSParticleShaderRainEmitter; { TracyD3D11Zone(state->tracyCtx, "Skylighting - Render Height Map"); + globals::profiler->BeginPass("Skylighting::OcclusionMask"); precip->RenderMask((RE::BSParticleShaderRainEmitter*)rain); + globals::profiler->EndPass(); } inOcclusion = false; diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index cfa1179070..b25d53ec24 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -255,7 +255,9 @@ void SubsurfaceScattering::DrawSSS() auto shader = GetComputeShaderHorizontalBlur(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::HorizontalBlur"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } uav = nullptr; @@ -274,7 +276,9 @@ void SubsurfaceScattering::DrawSSS() auto shader = GetComputeShaderVerticalBlur(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::VerticalBlur"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } } else if (settings.SSMode == 1) { // Burley pass to main texture @@ -284,7 +288,9 @@ void SubsurfaceScattering::DrawSSS() auto shader = GetComputeShaderBurley(); context->CSSetShader(shader, nullptr, 0); + globals::profiler->BeginPass("SubsurfaceScattering::Burley"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); context->CopyResource(main.texture, blurHorizontalTemp->resource.get()); } diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index 5adb6a5905..26cdd6ee89 100644 --- a/src/Features/TerrainBlending.cpp +++ b/src/Features/TerrainBlending.cpp @@ -806,7 +806,9 @@ void TerrainBlending::BlendPrepassDepths() context->CSSetShader(GetDepthBlendShader(), nullptr, 0); + globals::profiler->BeginPass("TerrainBlending::DepthBlend"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + globals::profiler->EndPass(); } ID3D11ShaderResourceView* views[2] = { nullptr, nullptr }; diff --git a/src/Features/TerrainShadows.cpp b/src/Features/TerrainShadows.cpp index b695d85e5c..b881a7836b 100644 --- a/src/Features/TerrainShadows.cpp +++ b/src/Features/TerrainShadows.cpp @@ -413,7 +413,9 @@ void TerrainShadows::UpdateShadow() context->CSSetUnorderedAccessViews(0, ARRAYSIZE(newer.uavs), newer.uavs, nullptr); context->CSSetConstantBuffers(0, 1, &newer.buffer); context->CSSetShader(shadowUpdateProgram.get(), nullptr, 0); + globals::profiler->BeginPass("TerrainShadows::ShadowUpdate"); context->Dispatch(abs(shadowUpdateCBData.LightPxDir.x) >= abs(shadowUpdateCBData.LightPxDir.y) ? height : width, 1, 1); + globals::profiler->EndPass(); /* ---- RESTORE ---- */ context->CSSetShaderResources(0, ARRAYSIZE(old.srvs), old.srvs); diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index b3a0190b6c..99c87ed7cc 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -1145,7 +1145,9 @@ void Upscaling::ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderRe ID3D11Buffer* cbs[1] = { vrClearHMDMaskCB.get() }; context->CSSetConstantBuffers(0, 1, cbs); + globals::profiler->BeginPass("Upscaling::ClearHMDMask"); context->Dispatch(dispatchX, dispatchY, 1); + globals::profiler->EndPass(); // Unbind ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; @@ -1415,7 +1417,9 @@ void Upscaling::CopySharedD3D12Resources() context->PSSetShader(copyDepthToSharedBufferPS.get(), nullptr, 0); + globals::profiler->BeginPass("Upscaling::CopyDepthD3D12"); context->Draw(3, 0); + globals::profiler->EndPass(); } // Clean up @@ -1710,6 +1714,7 @@ void Upscaling::Upscale() auto& motionVector = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; { + globals::profiler->BeginPass("Upscaling::EncodeTextures"); state->BeginPerfEvent("Encode Upscaling Textures"); TracyD3D11Zone(globals::state->tracyCtx, "Encode Upscaling Textures"); @@ -1769,9 +1774,11 @@ void Upscaling::Upscale() context->CSSetShader(shader, nullptr, 0); state->EndPerfEvent(); + globals::profiler->EndPass(); } { + globals::profiler->BeginPass("Upscaling::Upscale"); state->BeginPerfEvent("Upscaling"); TracyD3D11Zone(globals::state->tracyCtx, "Upscaling Dispatch"); @@ -1790,6 +1797,7 @@ void Upscaling::Upscale() } state->EndPerfEvent(); + globals::profiler->EndPass(); } } @@ -1948,7 +1956,9 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(2, rtvs, depth.views[0]); context->PSSetShader(depthUpscalePS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::DepthUpscale"); context->Draw(3, 0); + globals::profiler->EndPass(); } { @@ -1971,7 +1981,9 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); context->PSSetShader(underwaterMaskPS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::UnderwaterMaskUpscale"); context->Draw(3, 0); + globals::profiler->EndPass(); } // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. diff --git a/src/Features/Upscaling/RCAS/RCAS.cpp b/src/Features/Upscaling/RCAS/RCAS.cpp index d806e9d2c8..46db9ef92e 100644 --- a/src/Features/Upscaling/RCAS/RCAS.cpp +++ b/src/Features/Upscaling/RCAS/RCAS.cpp @@ -45,6 +45,7 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces return; } + globals::profiler->BeginPass("Upscaling::RCAS"); state->BeginPerfEvent("RCAS Sharpening"); uint32_t screenWidth = (uint32_t)state->screenSize.x; @@ -77,5 +78,6 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces context->CSSetShader(nullptr, nullptr, 0); + globals::profiler->EndPass(); state->EndPerfEvent(); } diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp index 63d5da8942..fd287fed39 100644 --- a/src/Features/VRStereoOptimizations.cpp +++ b/src/Features/VRStereoOptimizations.cpp @@ -379,7 +379,9 @@ void VRStereoOptimizations::DispatchStencil() uint32_t fullWidth = texPerPixelMode->desc.Width; uint32_t fullHeight = texPerPixelMode->desc.Height; + globals::profiler->BeginPass("VR::StencilClassify"); context->Dispatch((fullWidth + 7) / 8, (fullHeight + 7) / 8, 1); + globals::profiler->EndPass(); } // Cleanup CS bindings @@ -395,7 +397,9 @@ void VRStereoOptimizations::DispatchStencil() // Transfer classification to hardware stencil buffer { TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Stencil Write"); + globals::profiler->BeginPass("VR::StencilWrite"); ExecuteStencilWritePass(); + globals::profiler->EndPass(); } stencilActive = true; diff --git a/src/Features/VolumetricShadows.cpp b/src/Features/VolumetricShadows.cpp index 5bc31c8a04..34459618ed 100644 --- a/src/Features/VolumetricShadows.cpp +++ b/src/Features/VolumetricShadows.cpp @@ -1,5 +1,6 @@ #include "VolumetricShadows.h" +#include "Globals.h" #include "State.h" #include "Utils/D3D.h" @@ -193,13 +194,17 @@ void VolumetricShadows::CopyShadowLightData() ID3D11UnorderedAccessView* csUavs[1]{ shadowCopyMip0UAV }; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(downsampleShadowMip0CS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::DownsampleMip0"); context->Dispatch(dispatchSize, dispatchSize, 1); + globals::profiler->EndPass(); // Mip 1 (cascade 0) csUavs[0] = shadowCopyMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(downsampleShadowMip1CS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::DownsampleMip1"); context->Dispatch(dispatchSize, dispatchSize, 1); + globals::profiler->EndPass(); // Unbind SRVs before blur passes csSrvs[0] = nullptr; @@ -221,7 +226,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowBlurTempMip0UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowHorizontalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurHMip0"); context->Dispatch((mip0Size + GROUP_SIZE - 1) / GROUP_SIZE, mip0Size, 1); + globals::profiler->EndPass(); // Unbind for next pass blurSrvs[0] = nullptr; @@ -235,7 +242,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowCopyMip0UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowVerticalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurVMip0"); context->Dispatch(mip0Size, (mip0Size + GROUP_SIZE - 1) / GROUP_SIZE, 1); + globals::profiler->EndPass(); // Unbind blurSrvs[0] = nullptr; @@ -254,7 +263,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowBlurTempMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowHorizontalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurHMip1"); context->Dispatch((mip1Size + GROUP_SIZE - 1) / GROUP_SIZE, mip1Size, 1); + globals::profiler->EndPass(); // Unbind for next pass blurSrvs[0] = nullptr; @@ -268,7 +279,9 @@ void VolumetricShadows::CopyShadowLightData() csUavs[0] = shadowCopyMip1UAV; context->CSSetUnorderedAccessViews(0, 1, csUavs, nullptr); context->CSSetShader(blurShadowVerticalCS, nullptr, 0); + globals::profiler->BeginPass("VolumetricShadows::BlurVMip1"); context->Dispatch(mip1Size, (mip1Size + GROUP_SIZE - 1) / GROUP_SIZE, 1); + globals::profiler->EndPass(); // Unbind blurSrvs[0] = nullptr; diff --git a/src/Globals.cpp b/src/Globals.cpp index 34743140c5..b388d46f4a 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -155,6 +155,9 @@ namespace globals Menu* menu = nullptr; SIE::ShaderCache* shaderCache = nullptr; + static Profiler profilerInstance; + Profiler* profiler = &profilerInstance; + void OnInit() { shaderCache = &SIE::ShaderCache::Instance(); diff --git a/src/Globals.h b/src/Globals.h index 3e82ff0a2a..342f92ce4f 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -32,6 +32,7 @@ struct PerformanceOverlay; struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; +class Profiler; struct WeatherEditor; struct ExponentialHeightFog; struct HDRDisplay; @@ -261,6 +262,7 @@ namespace globals extern Deferred* deferred; extern Menu* menu; extern SIE::ShaderCache* shaderCache; + extern Profiler* profiler; void OnInit(); void ReInit(); diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 978984eb7d..8764cc3af1 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -16,6 +16,7 @@ #include "Globals.h" #include "Menu.h" #include "Menu/HomePageRenderer.h" +#include "Menu/ProfilingRenderer.h" #include "Menu/ThemeManager.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" @@ -26,7 +27,7 @@ namespace { // Core built-in menu names that always appear first in the menu list - constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "Display" }; + constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "Profiling", "Display" }; bool IsCoreMenu(const std::string& menuName) { @@ -280,7 +281,8 @@ std::vector FeatureListRenderer::BuildMenuLis auto menuList = std::vector{ BuiltInMenu{ "Home", []() { HomePageRenderer::RenderHomePage(); } }, BuiltInMenu{ "General", drawGeneralSettings }, - BuiltInMenu{ "Advanced", drawAdvancedSettings } + BuiltInMenu{ "Advanced", drawAdvancedSettings }, + BuiltInMenu{ "Profiling", []() { ProfilingRenderer::RenderStatistics(); } } }; // NOTE: The menu list is rebuilt every frame, so category expansion states // persist correctly. This is acceptable since the list is small and built // infrequently, but could be optimized if performance becomes an issue. @@ -751,6 +753,10 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImVec2 cursorPosBefore = ImGui::GetCursorPos(); feat->DrawSettings(); + + ImGui::SeparatorText("Profiling"); + ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); + ImVec2 cursorPosAfter = ImGui::GetCursorPos(); if (sceneControlled) diff --git a/src/Menu/ProfilingRenderer.cpp b/src/Menu/ProfilingRenderer.cpp new file mode 100644 index 0000000000..832fddb81b --- /dev/null +++ b/src/Menu/ProfilingRenderer.cpp @@ -0,0 +1,408 @@ +#include "ProfilingRenderer.h" + +#include +#include +#include +#include + +#include "Globals.h" +#include "State.h" + +static ImU32 HslToImU32(float h, float s, float l) +{ + auto hue2rgb = [](float p, float q, float t) -> float { + if (t < 0.0f) + t += 1.0f; + if (t > 1.0f) + t -= 1.0f; + if (t < 1.0f / 6.0f) + return p + (q - p) * 6.0f * t; + if (t < 0.5f) + return q; + if (t < 2.0f / 3.0f) + return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; + return p; + }; + + float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s; + float p = 2.0f * l - q; + float r = hue2rgb(p, q, h + 1.0f / 3.0f); + float g = hue2rgb(p, q, h); + float b = hue2rgb(p, q, h - 1.0f / 3.0f); + + return IM_COL32( + static_cast(r * 255.0f), + static_cast(g * 255.0f), + static_cast(b * 255.0f), + 255); +} + +static constexpr float kGoldenRatio = 0.618033988749895f; + +ImU32 ProfilingRenderer::GetGroupColor(const std::string& groupName) +{ + auto it = groupColorMap.find(groupName); + if (it != groupColorMap.end()) + return it->second; + + float hue = std::fmod(nextColorIndex * kGoldenRatio, 1.0f); + ImU32 color = HslToImU32(hue, 0.7f, 0.55f); + groupColorMap[groupName] = color; + nextColorIndex++; + return color; +} + +uint32_t ProfilingRenderer::ToLegitColor(ImU32 imColor) +{ + uint8_t r = (imColor >> 0) & 0xFF; + uint8_t g = (imColor >> 8) & 0xFF; + uint8_t b = (imColor >> 16) & 0xFF; + return (0xFF << 24) | (b << 16) | (g << 8) | r; +} + +ImVec4 ProfilingRenderer::HeatColor(float value, float maxValue) +{ + if (maxValue <= 0.0f) + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + + float x = std::clamp(value / maxValue, 0.0f, 1.0f); + + float x2 = x * x; + float x3 = x2 * x; + float x4 = x2 * x2; + float x5 = x3 * x2; + + float r = 0.13572138f + 4.61539260f * x - 42.66032258f * x2 + 132.13108234f * x3 - 152.94239396f * x4 + 59.28637943f * x5; + float g = 0.09140261f + 2.19418839f * x + 4.84296658f * x2 - 14.18503333f * x3 + 4.27729857f * x4 + 2.82956604f * x5; + float b = 0.10667330f + 12.64194608f * x - 60.58204836f * x2 + 110.36276771f * x3 - 89.90310912f * x4 + 27.34824973f * x5; + + float alpha = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg).w; + + return ImVec4(std::clamp(r, 0.0f, 1.0f), std::clamp(g, 0.0f, 1.0f), std::clamp(b, 0.0f, 1.0f), alpha); +} + +void ProfilingRenderer::TextHeat(const char* fmt, float value, float maxValue) +{ + ImVec4 bg = HeatColor(value, maxValue); + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ImGui::GetColorU32(bg)); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), fmt, value); +} + +void ProfilingRenderer::RenderGraph() +{ + auto& profiler = (*globals::profiler); + const auto& results = profiler.GetResults(); + bool cpuMode = (timingMode == TimingMode::CPU); + + if (results.empty()) + return; + + std::vector tasks; + + double accumulated = 0.0; + for (const auto& result : results) { + if (!result.valid) + continue; + + float timeMs = cpuMode ? result.cpuTimeMs : result.gpuTimeMs; + + std::string groupName; + auto pos = result.name.find("::"); + if (pos != std::string::npos) + groupName = result.name.substr(0, pos); + else + groupName = result.name; + + legit::ProfilerTask task; + task.startTime = accumulated / 1000.0; + task.endTime = (accumulated + timeMs) / 1000.0; + task.name = result.name; + task.color = ToLegitColor(GetGroupColor(groupName)); + tasks.push_back(task); + accumulated += timeMs; + } + + if (tasks.empty()) + return; + + gpuGraph.LoadFrameData(tasks.data(), tasks.size()); + + float maxFrameTimeSec = gpuGraph.GetPeakFrameTime() * 1.2f; + if (maxFrameTimeSec < 0.0001f) + maxFrameTimeSec = 0.0001f; + + float availWidth = ImGui::GetContentRegionAvail().x; + int legendWidth = 260; + int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); + int graphHeight = 180; + + gpuGraph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + + ImGui::Spacing(); +} + +void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) +{ + auto& profiler = (*globals::profiler); + + bool cpuMode = (timingMode == TimingMode::CPU); + if (showModeToggle) { + int mode = static_cast(timingMode); + ImGui::RadioButton("GPU", &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton("CPU", &mode, 1); + if (static_cast(mode) != timingMode) { + timingMode = static_cast(mode); + timeSinceLastUpdate = 1.0f; + } + cpuMode = (timingMode == TimingMode::CPU); + ImGui::Separator(); + } + + float currentTime = static_cast(ImGui::GetTime()); + float deltaTime = currentTime - lastFrameTime; + lastFrameTime = currentTime; + timeSinceLastUpdate += deltaTime; + + if (timeSinceLastUpdate >= 1.0f) { + timeSinceLastUpdate = 0.0f; + + cachedGroups.clear(); + cachedTotalAvgMs = 0.0f; + cachedTotalP95Ms = 0.0f; + cachedTotalP99Ms = 0.0f; + cachedMaxAvgMs = 0.0f; + cachedMaxP95Ms = 0.0f; + cachedMaxP99Ms = 0.0f; + std::unordered_map groupIndex; + + for (const auto& result : profiler.GetResults()) { + if (!result.valid) + continue; + + float avg = cpuMode ? result.cpuAvgMs : result.avgMs; + float p95 = cpuMode ? result.cpuP95Ms : result.p95Ms; + float p99 = cpuMode ? result.cpuP99Ms : result.p99Ms; + + cachedTotalAvgMs += avg; + cachedTotalP95Ms += p95; + cachedTotalP99Ms += p99; + + auto pos = result.name.find("::"); + if (pos != std::string::npos) { + std::string groupName = result.name.substr(0, pos); + std::string passLabel = result.name.substr(pos + 2); + + auto it = groupIndex.find(groupName); + if (it == groupIndex.end()) { + groupIndex[groupName] = cachedGroups.size(); + cachedGroups.push_back({ groupName, 0, 0, 0 }); + } + + auto& group = cachedGroups[groupIndex[groupName]]; + group.totalAvgMs += avg; + group.totalP95Ms += p95; + group.totalP99Ms += p99; + group.passes.push_back({ passLabel, avg, p95, p99 }); + } else { + groupIndex[result.name] = cachedGroups.size(); + cachedGroups.push_back({ result.name, avg, p95, p99 }); + } + } + + for (const auto& group : cachedGroups) { + cachedMaxAvgMs = std::max(cachedMaxAvgMs, group.totalAvgMs); + cachedMaxP95Ms = std::max(cachedMaxP95Ms, group.totalP95Ms); + cachedMaxP99Ms = std::max(cachedMaxP99Ms, group.totalP99Ms); + } + } + + if (cachedGroups.empty()) { + ImGui::TextDisabled("No timing data available (enter game world)"); + return; + } + + RenderGraph(); + + if (showTable) { + float availHeight = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing(); + + if (ImGui::BeginTable("##Profiler", 5, + ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX | ImGuiTableFlags_ScrollY, + ImVec2(0.0f, availHeight))) { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("%%", ImGuiTableColumnFlags_WidthFixed, 45.0f); + ImGui::TableHeadersRow(); + + for (const auto& group : cachedGroups) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + if (group.passes.empty()) { + ImGui::TreeNodeEx(group.name.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalAvgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (group.totalAvgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + } else { + bool open = ImGui::TreeNodeEx(group.name.c_str(), 0); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalAvgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", group.totalP99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (group.totalAvgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + if (open) { + for (const auto& pass : group.passes) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TreeNodeEx(pass.label.c_str(), ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.avgMs, cachedMaxAvgMs); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.p95Ms, cachedMaxP95Ms); + ImGui::TableNextColumn(); + TextHeat("%.3f", pass.p99Ms, cachedMaxP99Ms); + ImGui::TableNextColumn(); + if (cachedTotalAvgMs > 0.0f) + TextHeat("%5.1f", (pass.avgMs / cachedTotalAvgMs) * 100.0f, 100.0f); + } + ImGui::TreePop(); + } + } + } + ImGui::EndTable(); + } + } + +} + +void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) +{ + auto& profiler = (*globals::profiler); + const auto& results = profiler.GetResults(); + + int mode = static_cast(timingMode); + ImGui::RadioButton("GPU", &mode, 0); + ImGui::SameLine(); + ImGui::RadioButton("CPU", &mode, 1); + timingMode = static_cast(mode); + + bool cpuMode = (timingMode == TimingMode::CPU); + + struct Entry + { + std::string label; + float timeMs; + float avgMs; + float p95Ms; + float p99Ms; + }; + + std::vector entries; + float totalTimeMs = 0.0f; + float totalAvg = 0.0f; + float totalP95 = 0.0f; + float totalP99 = 0.0f; + float maxAvg = 0.0f; + float maxP95 = 0.0f; + float maxP99 = 0.0f; + + std::string prefix = featurePrefix + "::"; + for (const auto& r : results) { + if (!r.valid || !r.name.starts_with(prefix)) + continue; + std::string label = r.name.substr(prefix.size()); + float timeMs = cpuMode ? r.cpuTimeMs : r.gpuTimeMs; + float avg = cpuMode ? r.cpuAvgMs : r.avgMs; + float p95 = cpuMode ? r.cpuP95Ms : r.p95Ms; + float p99 = cpuMode ? r.cpuP99Ms : r.p99Ms; + entries.push_back({ label, timeMs, avg, p95, p99 }); + totalTimeMs += timeMs; + totalAvg += avg; + totalP95 += p95; + totalP99 += p99; + maxAvg = std::max(maxAvg, avg); + maxP95 = std::max(maxP95, p95); + maxP99 = std::max(maxP99, p99); + } + + if (entries.empty()) { + ImGui::TextDisabled("No timing data"); + return; + } + + auto& state = featureGraphs[featurePrefix]; + + std::vector tasks; + double accumulated = 0.0; + for (const auto& e : entries) { + legit::ProfilerTask task; + task.startTime = accumulated / 1000.0; + task.endTime = (accumulated + e.timeMs) / 1000.0; + task.name = e.label; + task.color = ToLegitColor(GetGroupColor(featurePrefix + "::" + e.label)); + tasks.push_back(task); + accumulated += e.timeMs; + } + + if (!tasks.empty()) { + state.graph.LoadFrameData(tasks.data(), tasks.size()); + + float maxFrameTimeSec = state.graph.GetPeakFrameTime() * 1.2f; + if (maxFrameTimeSec < 0.00001f) + maxFrameTimeSec = 0.00001f; + + float availWidth = ImGui::GetContentRegionAvail().x; + int legendWidth = 200; + int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); + int graphHeight = 100; + + state.graph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + ImGui::Spacing(); + } + + if (ImGui::BeginTable("##FeatureTimers", 4, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX)) { + ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); + ImGui::TableHeadersRow(); + + for (const auto& e : entries) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%s", e.label.c_str()); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.avgMs, maxAvg); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.p95Ms, maxP95); + ImGui::TableNextColumn(); + TextHeat("%.3f", e.p99Ms, maxP99); + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "Total"); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalAvg); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP95); + ImGui::TableNextColumn(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP99); + + ImGui::EndTable(); + } +} diff --git a/src/Menu/ProfilingRenderer.h b/src/Menu/ProfilingRenderer.h new file mode 100644 index 0000000000..11d11ac852 --- /dev/null +++ b/src/Menu/ProfilingRenderer.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include + +#include + +#include "Profiler.h" +#include "Utils/LegitProfiler.h" + +class ProfilingRenderer +{ +public: + enum class TimingMode + { + GPU, + CPU + }; + + static void RenderStatistics(bool showTable = true, bool showModeToggle = true); + static void RenderFeatureTimers(const std::string& featurePrefix); + +private: + static inline TimingMode timingMode = TimingMode::GPU; + static inline float timeSinceLastUpdate = 0.0f; + static inline float lastFrameTime = 0.0f; + + struct PassEntry + { + std::string label; + float avgMs; + float p95Ms; + float p99Ms; + }; + struct GroupEntry + { + std::string name; + float totalAvgMs = 0.0f; + float totalP95Ms = 0.0f; + float totalP99Ms = 0.0f; + std::vector passes; + }; + static inline float cachedTotalAvgMs = 0.0f; + static inline float cachedTotalP95Ms = 0.0f; + static inline float cachedTotalP99Ms = 0.0f; + static inline float cachedMaxAvgMs = 0.0f; + static inline float cachedMaxP95Ms = 0.0f; + static inline float cachedMaxP99Ms = 0.0f; + static inline std::vector cachedGroups; + + static inline ImGuiUtils::ProfilerGraph gpuGraph{ Profiler::kHistorySize }; + + struct FeatureGraphState + { + ImGuiUtils::ProfilerGraph graph{ Profiler::kHistorySize }; + }; + static inline std::unordered_map featureGraphs; + + static inline std::unordered_map groupColorMap; + static inline size_t nextColorIndex = 0; + + static ImU32 GetGroupColor(const std::string& groupName); + static uint32_t ToLegitColor(ImU32 imColor); + static ImVec4 HeatColor(float value, float maxValue); + static void TextHeat(const char* fmt, float value, float maxValue); + static void RenderGraph(); +}; diff --git a/src/Profiler.cpp b/src/Profiler.cpp new file mode 100644 index 0000000000..d5048fcb04 --- /dev/null +++ b/src/Profiler.cpp @@ -0,0 +1,236 @@ +#include "Profiler.h" + +#include +#include + +float Profiler::RollingHistory::GetAverage() const +{ + if (count == 0) + return lastMs; + float sum = 0.0f; + for (uint32_t i = 0; i < count; i++) + sum += history[i]; + return sum / static_cast(count); +} + +float Profiler::RollingHistory::GetPercentile(float p) const +{ + if (count == 0) + return lastMs; + + thread_local std::vector sorted; + sorted.resize(count); + for (uint32_t i = 0; i < count; i++) + sorted[i] = history[i]; + std::sort(sorted.begin(), sorted.end()); + + float idx = (p / 100.0f) * static_cast(count - 1); + uint32_t lo = static_cast(idx); + uint32_t hi = std::min(lo + 1, count - 1); + float frac = idx - static_cast(lo); + return sorted[lo] * (1.0f - frac) + sorted[hi] * frac; +} + +void Profiler::Initialize(ID3D11Device* device, ID3D11DeviceContext* a_context) +{ + Release(); + + context = a_context; + + LARGE_INTEGER freq; + QueryPerformanceFrequency(&freq); + cpuTicksToMs = 1000.0 / static_cast(freq.QuadPart); + + for (auto& frame : frames) { + D3D11_QUERY_DESC disjointDesc{}; + disjointDesc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; + device->CreateQuery(&disjointDesc, frame.disjoint.put()); + + frame.timers.resize(kMaxTimers); + for (auto& timer : frame.timers) { + D3D11_QUERY_DESC tsDesc{}; + tsDesc.Query = D3D11_QUERY_TIMESTAMP; + device->CreateQuery(&tsDesc, timer.begin.put()); + device->CreateQuery(&tsDesc, timer.end.put()); + } + frame.activeCount = 0; + frame.inFlight = false; + } + + writeFrame = 0; + readFrame = 0; + framesSinceInit = 0; + initialized = true; +} + +void Profiler::Release() +{ + for (auto& frame : frames) { + frame.disjoint = nullptr; + frame.timers.clear(); + frame.activeCount = 0; + frame.inFlight = false; + } + results.clear(); + knownTimers.clear(); + knownTimerIndex.clear(); + totalTimeMs = 0.0f; + cpuTotalTimeMs = 0.0f; + initialized = false; + context = nullptr; +} + +void Profiler::BeginFrame() +{ + if (!initialized || !context || frameActive) + return; + + CollectResults(); + + auto& frame = frames[writeFrame]; + frame.activeCount = 0; + frame.inFlight = true; + frameActive = true; + context->Begin(frame.disjoint.get()); +} + +void Profiler::BeginPass(const std::string& name) +{ + if (!initialized || !context) + return; + + if (!frameActive) + BeginFrame(); + + auto& frame = frames[writeFrame]; + if (frame.activeCount >= kMaxTimers) + return; + + auto& timer = frame.timers[frame.activeCount]; + timer.name = name; + context->End(timer.begin.get()); + QueryPerformanceCounter(&timer.cpuBegin); + + if (beginPerfEvent) + beginPerfEvent(name); +} + +void Profiler::EndPass() +{ + if (!initialized || !context || !frameActive) + return; + + auto& frame = frames[writeFrame]; + if (frame.activeCount >= kMaxTimers) + return; + + auto& timer = frame.timers[frame.activeCount]; + + LARGE_INTEGER cpuEnd; + QueryPerformanceCounter(&cpuEnd); + timer.cpuMs = static_cast(static_cast(cpuEnd.QuadPart - timer.cpuBegin.QuadPart) * cpuTicksToMs); + + context->End(timer.end.get()); + frame.activeCount++; + + if (endPerfEvent) + endPerfEvent({}); +} + +void Profiler::EndFrame() +{ + if (!initialized || !context || !frameActive) + return; + + frameActive = false; + context->End(frames[writeFrame].disjoint.get()); + writeFrame = (writeFrame + 1) % kFrameLatency; + framesSinceInit++; +} + +void Profiler::CollectResults() +{ + if (framesSinceInit < kFrameLatency) + return; + + readFrame = writeFrame; + auto& frame = frames[readFrame]; + if (!frame.inFlight) + return; + + D3D11_QUERY_DATA_TIMESTAMP_DISJOINT disjointData{}; + HRESULT hr = context->GetData(frame.disjoint.get(), &disjointData, sizeof(disjointData), D3D11_ASYNC_GETDATA_DONOTFLUSH); + if (hr != S_OK) + return; + + frame.inFlight = false; + + struct ActiveTimerData + { + float gpuMs = 0.0f; + float cpuMs = 0.0f; + }; + std::unordered_map activeTimers; + float activeTotalMs = 0.0f; + float activeCpuTotalMs = 0.0f; + + if (!disjointData.Disjoint) { + double ticksToMs = 1000.0 / static_cast(disjointData.Frequency); + + for (uint32_t i = 0; i < frame.activeCount; i++) { + auto& timer = frame.timers[i]; + UINT64 tsBegin = 0, tsEnd = 0; + + if (context->GetData(timer.begin.get(), &tsBegin, sizeof(tsBegin), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK) + continue; + if (context->GetData(timer.end.get(), &tsEnd, sizeof(tsEnd), D3D11_ASYNC_GETDATA_DONOTFLUSH) != S_OK) + continue; + + float ms = static_cast(static_cast(tsEnd - tsBegin) * ticksToMs); + auto& entry = activeTimers[timer.name]; + entry.gpuMs += ms; + entry.cpuMs += timer.cpuMs; + activeTotalMs += ms; + activeCpuTotalMs += timer.cpuMs; + + auto [it, inserted] = knownTimerIndex.try_emplace(timer.name, knownTimers.size()); + if (inserted) { + KnownTimer kt; + kt.name = timer.name; + knownTimers.push_back(std::move(kt)); + } + auto& known = knownTimers[it->second]; + known.gpu.PushSample(ms); + known.cpu.PushSample(timer.cpuMs); + } + } + + totalTimeMs = activeTotalMs; + cpuTotalTimeMs = activeCpuTotalMs; + + results.clear(); + results.reserve(knownTimers.size()); + for (const auto& known : knownTimers) { + TimerResult result; + result.name = known.name; + auto it = activeTimers.find(known.name); + if (it != activeTimers.end()) { + result.gpuTimeMs = it->second.gpuMs; + result.cpuTimeMs = it->second.cpuMs; + } else { + result.gpuTimeMs = known.gpu.lastMs; + result.cpuTimeMs = known.cpu.lastMs; + } + result.avgMs = known.gpu.GetAverage(); + result.p95Ms = known.gpu.GetPercentile(95.0f); + result.p99Ms = known.gpu.GetPercentile(99.0f); + result.cpuAvgMs = known.cpu.GetAverage(); + result.cpuP95Ms = known.cpu.GetPercentile(95.0f); + result.cpuP99Ms = known.cpu.GetPercentile(99.0f); + result.valid = true; + result.historyBuffer = known.gpu.history; + result.historyHead = known.gpu.head; + result.historyCount = known.gpu.count; + results.push_back(std::move(result)); + } +} diff --git a/src/Profiler.h b/src/Profiler.h new file mode 100644 index 0000000000..0b42166875 --- /dev/null +++ b/src/Profiler.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class Profiler +{ +public: + static constexpr uint32_t kMaxTimers = 128; + static constexpr uint32_t kFrameLatency = 3; + static constexpr uint32_t kHistorySize = 300; + + using PerfEventCallback = std::function; + + struct RollingHistory + { + float history[kHistorySize]{}; + uint32_t head = 0; + uint32_t count = 0; + float lastMs = 0.0f; + + void PushSample(float ms) + { + history[head] = ms; + head = (head + 1) % kHistorySize; + if (count < kHistorySize) + count++; + lastMs = ms; + } + + float GetAverage() const; + float GetPercentile(float p) const; + }; + + struct TimerResult + { + std::string name; + float gpuTimeMs = 0.0f; + float avgMs = 0.0f; + float p95Ms = 0.0f; + float p99Ms = 0.0f; + float cpuTimeMs = 0.0f; + float cpuAvgMs = 0.0f; + float cpuP95Ms = 0.0f; + float cpuP99Ms = 0.0f; + bool valid = false; + + const float* historyBuffer = nullptr; + uint32_t historyHead = 0; + uint32_t historyCount = 0; + + float GetHistorySample(uint32_t index) const + { + if (!historyBuffer || index >= historyCount) + return 0.0f; + return historyBuffer[(historyHead - historyCount + index + kHistorySize) % kHistorySize]; + } + }; + + void Initialize(ID3D11Device* device, ID3D11DeviceContext* context); + void Release(); + + void SetPerfEventCallbacks(PerfEventCallback beginCb, PerfEventCallback endCb) + { + beginPerfEvent = std::move(beginCb); + endPerfEvent = std::move(endCb); + } + + void BeginFrame(); + void BeginPass(const std::string& name); + void EndPass(); + void EndFrame(); + + const std::vector& GetResults() const { return results; } + float GetTotalTimeMs() const { return totalTimeMs; } + float GetCpuTotalTimeMs() const { return cpuTotalTimeMs; } + + void ClearTimers() + { + results.clear(); + knownTimers.clear(); + knownTimerIndex.clear(); + totalTimeMs = 0.0f; + cpuTotalTimeMs = 0.0f; + } + + void ClearTimersForFeature(const std::string& featureName) + { + std::string prefix = featureName + "::"; + std::erase_if(knownTimers, [&prefix](const KnownTimer& kt) { + return kt.name.starts_with(prefix); + }); + knownTimerIndex.clear(); + for (size_t i = 0; i < knownTimers.size(); i++) + knownTimerIndex[knownTimers[i].name] = i; + } + +private: + struct FrameQueries + { + winrt::com_ptr disjoint; + struct TimerPair + { + winrt::com_ptr begin; + winrt::com_ptr end; + std::string name; + LARGE_INTEGER cpuBegin{}; + float cpuMs = 0.0f; + }; + std::vector timers; + uint32_t activeCount = 0; + bool inFlight = false; + }; + + ID3D11DeviceContext* context = nullptr; + + FrameQueries frames[kFrameLatency]; + uint32_t writeFrame = 0; + uint32_t readFrame = 0; + uint32_t framesSinceInit = 0; + bool initialized = false; + bool frameActive = false; + double cpuTicksToMs = 0.0; + + PerfEventCallback beginPerfEvent; + PerfEventCallback endPerfEvent; + + std::vector results; + + struct KnownTimer + { + std::string name; + RollingHistory gpu; + RollingHistory cpu; + }; + std::vector knownTimers; + std::unordered_map knownTimerIndex; + float totalTimeMs = 0.0f; + float cpuTotalTimeMs = 0.0f; + + void CollectResults(); +}; diff --git a/src/State.cpp b/src/State.cpp index 753f2ce18c..058bff6233 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -164,6 +164,8 @@ void State::Debug() void State::Reset() { + globals::profiler->EndFrame(); + Feature::ForEachLoadedFeature("Reset", [](Feature* feature) { feature->Reset(); }); if (!globals::game::ui->GameIsPaused()) timer += RE::GetSecondsSinceLastFrame(); @@ -755,6 +757,16 @@ void State::SetupResources() #ifdef TRACY_ENABLE Feature::SetTracyCtx(tracyCtx); #endif + + globals::profiler->Initialize(globals::d3d::device, globals::d3d::context); + + if (frameAnnotations) { + globals::profiler->SetPerfEventCallbacks( + [this](std::string_view name) { BeginPerfEvent(name); }, + [this](std::string_view) { EndPerfEvent(); }); + } else { + globals::profiler->SetPerfEventCallbacks({}, {}); + } } void State::ModifyShaderLookup(const RE::BSShader& a_shader, uint& a_vertexDescriptor, uint& a_pixelDescriptor, bool a_forceDeferred) diff --git a/src/Utils/LegitProfiler.h b/src/Utils/LegitProfiler.h new file mode 100644 index 0000000000..1a766f88da --- /dev/null +++ b/src/Utils/LegitProfiler.h @@ -0,0 +1,292 @@ +// Based on LegitProfiler by Raikiri (https://github.com/Raikiri/LegitProfiler) +// MIT License - modified to remove glm dependency, using ImVec2 directly. +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace legit +{ + namespace Colors + { +#define RGBA_LE(col) (((col & 0xff000000) >> (3 * 8)) + ((col & 0x00ff0000) >> (1 * 8)) + ((col & 0x0000ff00) << (1 * 8)) + ((col & 0x000000ff) << (3 * 8))) + const static uint32_t turqoise = RGBA_LE(0x1abc9cffu); + const static uint32_t greenSea = RGBA_LE(0x16a085ffu); + const static uint32_t emerald = RGBA_LE(0x2ecc71ffu); + const static uint32_t nephritis = RGBA_LE(0x27ae60ffu); + const static uint32_t peterRiver = RGBA_LE(0x3498dbffu); + const static uint32_t belizeHole = RGBA_LE(0x2980b9ffu); + const static uint32_t amethyst = RGBA_LE(0x9b59b6ffu); + const static uint32_t wisteria = RGBA_LE(0x8e44adffu); + const static uint32_t sunFlower = RGBA_LE(0xf1c40fffu); + const static uint32_t orange = RGBA_LE(0xf39c12ffu); + const static uint32_t carrot = RGBA_LE(0xe67e22ffu); + const static uint32_t pumpkin = RGBA_LE(0xd35400ffu); + const static uint32_t alizarin = RGBA_LE(0xe74c3cffu); + const static uint32_t pomegranate = RGBA_LE(0xc0392bffu); + const static uint32_t clouds = RGBA_LE(0xecf0f1ffu); + const static uint32_t silver = RGBA_LE(0xbdc3c7ffu); + const static uint32_t imguiText = RGBA_LE(0xF2F5FAFFu); +#undef RGBA_LE + } + + struct ProfilerTask + { + double startTime; + double endTime; + std::string name; + uint32_t color; + double GetLength() { return endTime - startTime; } + }; +} + +namespace ImGuiUtils +{ + class ProfilerGraph + { + public: + int frameWidth; + int frameSpacing; + bool useColoredLegendText; + + ProfilerGraph(size_t framesCount) + { + frames.resize(framesCount); + for (auto& frame : frames) + frame.tasks.reserve(100); + frameWidth = 3; + frameSpacing = 1; + useColoredLegendText = false; + } + + void LoadFrameData(const legit::ProfilerTask* tasks, size_t count) + { + auto& currFrame = frames[currFrameIndex]; + currFrame.tasks.resize(0); + currFrame.totalTime = 0.0f; + for (size_t taskIndex = 0; taskIndex < count; taskIndex++) { + if (taskIndex == 0) + currFrame.tasks.push_back(tasks[taskIndex]); + else { + if (tasks[taskIndex - 1].color != tasks[taskIndex].color || tasks[taskIndex - 1].name != tasks[taskIndex].name) + currFrame.tasks.push_back(tasks[taskIndex]); + else + currFrame.tasks.back().endTime = tasks[taskIndex].endTime; + } + currFrame.totalTime += float(tasks[taskIndex].endTime - tasks[taskIndex].startTime); + } + currFrame.taskStatsIndex.resize(currFrame.tasks.size()); + + for (size_t taskIndex = 0; taskIndex < currFrame.tasks.size(); taskIndex++) { + auto& task = currFrame.tasks[taskIndex]; + auto it = taskNameToStatsIndex.find(task.name); + if (it == taskNameToStatsIndex.end()) { + taskNameToStatsIndex[task.name] = taskStats.size(); + TaskStats taskStat; + taskStat.maxTime = -1.0; + taskStats.push_back(taskStat); + } + currFrame.taskStatsIndex[taskIndex] = taskNameToStatsIndex[task.name]; + } + { + float recentMax = 0.0f; + size_t lookback = std::min(frames.size(), size_t(120)); + for (size_t i = 0; i < lookback; i++) { + size_t idx = (currFrameIndex + frames.size() - 1 - i) % frames.size(); + recentMax = std::max(recentMax, frames[idx].totalTime); + } + if (peakFrameTime <= 0.0f) + peakFrameTime = recentMax; + else { + float rate = (recentMax < peakFrameTime) ? 0.02f : 0.01f; + peakFrameTime += (recentMax - peakFrameTime) * rate; + } + } + currFrameIndex = (currFrameIndex + 1) % frames.size(); + RebuildTaskStats(currFrameIndex, 300); + } + + float GetTotalTaskTime(int frameIndexOffset) + { + return frames[GetCurrFrameIndex(frameIndexOffset)].totalTime; + } + + float GetPeakFrameTime() const { return peakFrameTime; } + + void RenderTimings(int graphWidth, int legendWidth, int height, int frameIndexOffset, float maxFrameTime) + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImVec2 widgetPos = ImGui::GetCursorScreenPos(); + RenderGraph(drawList, widgetPos, ImVec2(float(graphWidth), float(height)), frameIndexOffset, maxFrameTime); + RenderLegend(drawList, ImVec2(widgetPos.x + graphWidth, widgetPos.y), ImVec2(float(legendWidth), float(height)), frameIndexOffset, maxFrameTime); + ImGui::Dummy(ImVec2(float(graphWidth + legendWidth), float(height))); + } + + private: + size_t GetCurrFrameIndex(size_t frameIndexOffset) + { + return (currFrameIndex - frameIndexOffset - 1 + 2 * frames.size()) % frames.size(); + } + + void RebuildTaskStats(size_t endFrame, size_t framesCount) + { + for (auto& taskStat : taskStats) { + taskStat.maxTime = -1.0f; + taskStat.priorityOrder = size_t(-1); + taskStat.onScreenIndex = size_t(-1); + } + + for (size_t frameNumber = 0; frameNumber < framesCount; frameNumber++) { + size_t frameIndex = (endFrame - 1 - frameNumber + frames.size()) % frames.size(); + auto& frame = frames[frameIndex]; + for (size_t taskIndex = 0; taskIndex < frame.tasks.size(); taskIndex++) { + auto& task = frame.tasks[taskIndex]; + auto& stats = taskStats[frame.taskStatsIndex[taskIndex]]; + stats.maxTime = std::max(stats.maxTime, task.endTime - task.startTime); + } + } + std::vector statPriorities; + statPriorities.resize(taskStats.size()); + for (size_t statIndex = 0; statIndex < taskStats.size(); statIndex++) + statPriorities[statIndex] = statIndex; + + std::sort(statPriorities.begin(), statPriorities.end(), [this](size_t left, size_t right) { return taskStats[left].maxTime > taskStats[right].maxTime; }); + for (size_t statNumber = 0; statNumber < taskStats.size(); statNumber++) { + size_t statIndex = statPriorities[statNumber]; + taskStats[statIndex].priorityOrder = statNumber; + } + } + + void RenderGraph(ImDrawList* drawList, ImVec2 graphPos, ImVec2 graphSize, size_t frameIndexOffset, float maxFrameTime) + { + Rect(drawList, graphPos, ImVec2(graphPos.x + graphSize.x, graphPos.y + graphSize.y), 0xffffffff, false); + float heightThreshold = 1.0f; + + for (size_t frameNumber = 0; frameNumber < frames.size(); frameNumber++) { + size_t frameIndex = GetCurrFrameIndex(frameIndexOffset + frameNumber); + + ImVec2 framePos = ImVec2(graphPos.x + graphSize.x - 1 - frameWidth - (frameWidth + frameSpacing) * float(frameNumber), graphPos.y + graphSize.y - 1); + if (framePos.x < graphPos.x + 1) + break; + ImVec2 taskPos = framePos; + auto& frame = frames[frameIndex]; + for (const auto& task : frame.tasks) { + float taskStartHeight = (float(task.startTime) / maxFrameTime) * graphSize.y; + float taskEndHeight = (float(task.endTime) / maxFrameTime) * graphSize.y; + if (std::abs(taskEndHeight - taskStartHeight) > heightThreshold) + Rect(drawList, ImVec2(taskPos.x, taskPos.y - taskStartHeight), ImVec2(taskPos.x + frameWidth, taskPos.y - taskEndHeight), task.color, true); + } + } + } + + void RenderLegend(ImDrawList* drawList, ImVec2 legendPos, ImVec2 legendSize, size_t frameIndexOffset, float maxFrameTime) + { + float markerLeftRectMargin = 3.0f; + float markerLeftRectWidth = 5.0f; + float markerMidWidth = 30.0f; + float markerRightRectWidth = 10.0f; + float markerRigthRectMargin = 3.0f; + float markerRightRectHeight = 10.0f; + float markerRightRectSpacing = 4.0f; + float nameOffset = 30.0f; + ImVec2 textMargin = ImVec2(5.0f, -3.0f); + + auto& currFrame = frames[GetCurrFrameIndex(frameIndexOffset)]; + size_t maxTasksCount = size_t(legendSize.y / (markerRightRectHeight + markerRightRectSpacing)); + + for (auto& taskStat : taskStats) + taskStat.onScreenIndex = size_t(-1); + + size_t tasksToShow = std::min(taskStats.size(), maxTasksCount); + size_t tasksShownCount = 0; + for (size_t taskIndex = 0; taskIndex < currFrame.tasks.size(); taskIndex++) { + auto& task = currFrame.tasks[taskIndex]; + auto& stat = taskStats[currFrame.taskStatsIndex[taskIndex]]; + + if (stat.priorityOrder >= tasksToShow) + continue; + + if (stat.onScreenIndex == size_t(-1)) + stat.onScreenIndex = tasksShownCount++; + else + continue; + + float taskStartHeight = (float(task.startTime) / maxFrameTime) * legendSize.y; + float taskEndHeight = (float(task.endTime) / maxFrameTime) * legendSize.y; + + ImVec2 markerLeftRectMin = ImVec2(legendPos.x + markerLeftRectMargin, legendPos.y + legendSize.y); + ImVec2 markerLeftRectMax = ImVec2(markerLeftRectMin.x + markerLeftRectWidth, markerLeftRectMin.y); + markerLeftRectMin.y -= taskStartHeight; + markerLeftRectMax.y -= taskEndHeight; + + ImVec2 markerRightRectMin = ImVec2(legendPos.x + markerLeftRectMargin + markerLeftRectWidth + markerMidWidth, legendPos.y + legendSize.y - markerRigthRectMargin - (markerRightRectHeight + markerRightRectSpacing) * float(stat.onScreenIndex)); + ImVec2 markerRightRectMax = ImVec2(markerRightRectMin.x + markerRightRectWidth, markerRightRectMin.y - markerRightRectHeight); + RenderTaskMarker(drawList, markerLeftRectMin, markerLeftRectMax, markerRightRectMin, markerRightRectMax, task.color); + + uint32_t textColor = useColoredLegendText ? task.color : legit::Colors::imguiText; + + float taskTimeMs = float(task.endTime - task.startTime); + std::ostringstream timeText; + timeText.precision(2); + timeText << std::fixed << std::string("[") << (taskTimeMs * 1000.0f); + + Text(drawList, ImVec2(markerRightRectMax.x + textMargin.x, markerRightRectMax.y + textMargin.y), textColor, timeText.str().c_str()); + Text(drawList, ImVec2(markerRightRectMax.x + textMargin.x + nameOffset, markerRightRectMax.y + textMargin.y), textColor, (std::string("ms] ") + task.name).c_str()); + } + } + + static void Rect(ImDrawList* drawList, ImVec2 minPoint, ImVec2 maxPoint, uint32_t col, bool filled = true) + { + if (filled) + drawList->AddRectFilled(minPoint, maxPoint, col); + else + drawList->AddRect(minPoint, maxPoint, col); + } + + static void Text(ImDrawList* drawList, ImVec2 point, uint32_t col, const char* text) + { + drawList->AddText(point, col, text); + } + + static void RenderTaskMarker(ImDrawList* drawList, ImVec2 leftMinPoint, ImVec2 leftMaxPoint, ImVec2 rightMinPoint, ImVec2 rightMaxPoint, uint32_t col) + { + Rect(drawList, leftMinPoint, leftMaxPoint, col, true); + Rect(drawList, rightMinPoint, rightMaxPoint, col, true); + std::array points = { + ImVec2(leftMaxPoint.x, leftMinPoint.y), + ImVec2(leftMaxPoint.x, leftMaxPoint.y), + ImVec2(rightMinPoint.x, rightMaxPoint.y), + ImVec2(rightMinPoint.x, rightMinPoint.y) + }; + drawList->AddConvexPolyFilled(points.data(), int(points.size()), col); + } + + struct FrameData + { + std::vector tasks; + std::vector taskStatsIndex; + float totalTime; + }; + + struct TaskStats + { + double maxTime; + size_t priorityOrder; + size_t onScreenIndex; + }; + + std::vector taskStats; + std::map taskNameToStatsIndex; + std::vector frames; + size_t currFrameIndex = 0; + float peakFrameTime = 0.0f; + }; +} From e259a920bff3b8108523f21b8c0c26c9afc30b8c Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Sun, 31 May 2026 14:13:52 +0200 Subject: [PATCH 05/55] chore(sky-sync): move to core (#2418) --- features/Sky Sync/CORE | 0 features/Sky Sync/Shaders/Features/SkySync.ini | 6 ------ src/Features/SkySync.h | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) create mode 100644 features/Sky Sync/CORE diff --git a/features/Sky Sync/CORE b/features/Sky Sync/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Sky Sync/Shaders/Features/SkySync.ini b/features/Sky Sync/Shaders/Features/SkySync.ini index 1323a48697..5dd39c9cbd 100644 --- a/features/Sky Sync/Shaders/Features/SkySync.ini +++ b/features/Sky Sync/Shaders/Features/SkySync.ini @@ -1,8 +1,2 @@ [Info] Version = 1-1-0 - -[Nexus] -nexusmodid = 153543 -nexusfilegroupid = 000000 -nexusfilename = Sky Sync -autoupload = true diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index c1c1eed86c..efe6831898 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -47,6 +47,7 @@ struct SkySync : Feature virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; + virtual bool IsCore() const override { return true; } virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; From ca1c494f265971b79a1450783c7dfbf6a7907b80 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 31 May 2026 14:14:00 +0100 Subject: [PATCH 06/55] fix(hooks): promote snow render targets to fp16 for banding (#2410) --- src/Hooks.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 4c8345a51b..a4d6edbb3a 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -515,6 +515,29 @@ namespace Hooks static inline REL::Relocation func; }; + // kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader + // writes accumulated wetness/sparkle values that exceed the 8-bit range and + // quantize into visible banding on tessellated snow. Promote to fp16 for headroom. + struct CreateRenderTarget_Snow + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, a_properties); + } + static inline REL::Relocation func; + }; + + struct CreateRenderTarget_SnowSwap + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, a_properties); + } + static inline REL::Relocation func; + }; + // kNORMAL_TAAMASK_SSRMASK and its swap need UAV bind because DeferredCompositeCS // writes vanilla-encoded normals through UAV1 (`normals.UAV` in Deferred::DeferredPasses), // which feeds the post-pass vanilla SSAO chain (ISSAORawAO -> ISSAOComposite). Without @@ -859,6 +882,8 @@ namespace Hooks logger::info("Hooking BSShaderRenderTargets::Create::CreateRenderTarget(s)"); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3, 0x548)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409, 0x55E)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x41C, 0x41F, 0x574)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B, 0x5B0)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E)); From 6d4d8ad700c840ea8a45387c937c4d3a128a2060 Mon Sep 17 00:00:00 2001 From: jiayev Date: Mon, 1 Jun 2026 16:51:24 +0800 Subject: [PATCH 07/55] feat: advanced skin (#2428) --- features/Skin/Shaders/Features/Skin.ini | 2 + features/Skin/Shaders/Skin/Skin.hlsli | 300 +++++++++ features/Skin/Shaders/Skin/skin_detail_n.dds | Bin 0 -> 1398276 bytes include/DynamicWetness_PublicAPI.h | 350 +++++++++++ package/Shaders/Common/LightingCommon.hlsli | 11 + package/Shaders/Common/LightingEval.hlsli | 29 + package/Shaders/Common/SharedData.hlsli | 12 + package/Shaders/Lighting.hlsl | 171 ++++- src/Feature.cpp | 4 +- src/FeatureBuffer.cpp | 4 +- src/Features/Skin.cpp | 625 +++++++++++++++++++ src/Features/Skin.h | 145 +++++ src/Features/TerrainHelper.cpp | 2 +- src/Features/TerrainHelper.h | 2 +- src/Globals.cpp | 2 + src/Globals.h | 2 + src/Hooks.cpp | 1 + src/State.cpp | 15 +- src/TruePBR.cpp | 2 +- src/TruePBR.h | 2 +- 20 files changed, 1670 insertions(+), 11 deletions(-) create mode 100644 features/Skin/Shaders/Features/Skin.ini create mode 100644 features/Skin/Shaders/Skin/Skin.hlsli create mode 100644 features/Skin/Shaders/Skin/skin_detail_n.dds create mode 100644 include/DynamicWetness_PublicAPI.h create mode 100644 src/Features/Skin.cpp create mode 100644 src/Features/Skin.h diff --git a/features/Skin/Shaders/Features/Skin.ini b/features/Skin/Shaders/Features/Skin.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Skin/Shaders/Features/Skin.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/features/Skin/Shaders/Skin/Skin.hlsli b/features/Skin/Shaders/Skin/Skin.hlsli new file mode 100644 index 0000000000..312973e8ad --- /dev/null +++ b/features/Skin/Shaders/Skin/Skin.hlsli @@ -0,0 +1,300 @@ +#ifndef __SKIN_HLSLI__ +#define __SKIN_HLSLI__ + +#include "Common/BRDF.hlsli" +#include "Common/Color.hlsli" +#include "Common/LightingCommon.hlsli" +#include "Common/Math.hlsli" +#include "Common/Shading.hlsli" +#include "Common/SharedData.hlsli" + +namespace Skin +{ + float CalculateCurvature(float3 N) + { + const float3 dNdx = ddx(N); + const float3 dNdy = ddy(N); + return length(float2(dot(dNdx, dNdx), dot(dNdy, dNdy))); + } + +#if defined(PSHADER) + cbuffer SkinPerGeometry : register(b7) + { + float4 skinPerGeometry; + }; +#endif +#if defined(SKIN) + Texture2D TexSkinDetailNormal : register(t72); + + // [Jorge Jimenez, Diego Gutierrez 2015, "Separable Subsurface Scattering"] + // https://www.iryoku.com/separable-sss/ + float3 SSSSTransmittance(float translucency, float sssWidth, float3 worldNormal, float3 light, float d) + { + /** + * Calculate the scale of the effect. + */ + float scale = 8.25 * (1.0 - translucency) / sssWidth; + + /** + * First we shrink the position inwards the surface to avoid artifacts: + * (Note that this can be done once for all the lights) + */ + // float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0); + + /** + * Now we calculate the thickness from the light point of view: + */ + // float4 shadowPosition = mul(shrinkedPos, lightViewProjection); + // float d1 = SSSSSampleShadowmap(shadowPosition.xy / shadowPosition.w).r; // 'd1' has a range of 0..1 + // float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane' + // d1 *= lightFarPlane; // So we scale 'd1' accordingly: + // float d = scale * abs(d1 - d2); + d = scale * abs(d); // Use the passed 'd' value instead of calculating it here. + + /** + * Armed with the thickness, we can now calculate the color by means of the + * precalculated transmittance profile. + * (It can be precomputed into a texture, for maximum performance): + */ + float dd = -d * d; + float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) + + float3(0.1, 0.336, 0.344) * exp(dd / 0.0484) + + float3(0.118, 0.198, 0.0) * exp(dd / 0.187) + + float3(0.113, 0.007, 0.007) * exp(dd / 0.567) + + float3(0.358, 0.004, 0.0) * exp(dd / 1.99) + + float3(0.078, 0.0, 0.0) * exp(dd / 7.41); + + /** + * Using the profile, we finally approximate the transmitted lighting from + * the back of the object: + */ + return profile * saturate(0.3 + dot(light, -worldNormal)); + } + + float3 DualSpecularGGX(float AverageRoughness, float Lobe0Roughness, float Lobe1Roughness, float LobeMix, float3 SpecularColor, float NdotL, float NdotV, float NdotH, float VdotH, out float3 F) + { + float D = lerp(BRDF::D_GGX(Lobe0Roughness, NdotH), BRDF::D_GGX(Lobe1Roughness, NdotH), LobeMix); + float G = BRDF::Vis_SmithJointApprox(AverageRoughness, NdotV, NdotL); + F = BRDF::F_Schlick(SpecularColor, VdotH); + + return D * G * F; + } + + // a contact shadow approximation, totally not physically correct; a riff on "Chan 2018, "Material Advances in Call of Duty: WWII" and "The Technical Art of Uncharted 4" http://advances.realtimerendering.com/other/2016/naughty_dog/NaughtyDog_TechArt_Final.pdf (microshadowing)" + float ApproximateDirectOcculusion(float aoVisibility, float NdotL) + { + float aperture = rsqrt(1.0000001 - aoVisibility); + NdotL += 0.1; // when using bent normals, avoids overshadowing - bent normals are just approximation anyhow + return saturate(NdotL * aperture); + } + + void SkinDirectLightInput( + out DirectLightingOutput lightingOutput, + DirectContext context, + MaterialProperties material) + { + lightingOutput = (DirectLightingOutput)0; + context.lightColor *= Color::PBRLightingCompensation * context.detailedShadow; + + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 L = context.lightDir; + const float3 H = context.halfVector; + + const float oNdotL = dot(N, L); + float NdotL = clamp(oNdotL, 1e-5, 1.0); + float NdotV = saturate(abs(dot(N, V)) + 1e-5); + float NdotH = saturate(dot(N, H)); + float VdotH = saturate(dot(V, H)); + + context.lightColor *= ApproximateDirectOcculusion(material.AO, NdotL); + + float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); + + lightingOutput.diffuse += context.lightColor * NdotL * BRDF::Diffuse_Burley(averageRoughness, NdotV, NdotL, VdotH); + float3 F; + float3 F0 = material.F0 * saturate(1 - material.Curvature); + + lightingOutput.specular += DualSpecularGGX(averageRoughness, material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity, F0, NdotL, NdotV, NdotH, VdotH, F) * context.lightColor * NdotL; + + float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); + lightingOutput.specular *= 1 + F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + lightingOutput.diffuse *= 1 - F; + + if (material.FuzzWeight > 0.0) { + float3 FuzzF0 = material.FuzzColor * saturate(1 - material.Curvature); + float fuzzD = BRDF::D_Charlie(material.FuzzRoughness, NdotH); + float fuzzG = BRDF::Vis_Neubelt(NdotV, NdotL); + float3 fuzzF = BRDF::F_Schlick(FuzzF0, VdotH); + float3 fuzzSpecular = fuzzD * fuzzG * fuzzF * context.lightColor * NdotL; + float2 fuzzSpecularBRDF = BRDF::EnvBRDFApproxLazarov(material.FuzzRoughness, NdotV); + fuzzSpecular *= 1 + material.FuzzColor * (1 / (fuzzSpecularBRDF.x + fuzzSpecularBRDF.y) - 1); + + lightingOutput.specular += fuzzSpecular * material.FuzzWeight; + } + + float3 sssTransmittance = SSSSTransmittance( + SharedData::skinData.sssParams.x, + SharedData::skinData.sssParams.y, + N, + L, + material.Thickness) * + SharedData::skinData.sssParams.w; + lightingOutput.transmission = min(sssTransmittance * context.lightColor * context.softShadow * material.BaseColor, context.lightColor); + } + + void SkinIndirectLobeWeights( + out IndirectLobeWeights lobeWeights, + MaterialProperties material, + IndirectContext context) + { + lobeWeights = (IndirectLobeWeights)0; + + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 VN = context.vertexNormal; + + float NdotV = saturate(dot(N, V)); + + float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); + + float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); + + lobeWeights.specular = material.F0 * specularBRDF.x + specularBRDF.y; + + lobeWeights.diffuse = material.BaseColor * (1.0 - lobeWeights.specular.x - lobeWeights.specular.y); + lobeWeights.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + + float3 R = reflect(-V, N); + float horizon = min(1.0 + dot(R, VN), 1.0); + horizon *= horizon; + lobeWeights.specular *= horizon; + + float3 diffuseAO = material.AO; + float3 specularAO = SpecularAOLagarde(NdotV, material.AO, averageRoughness); + + diffuseAO = MultiBounceAO(material.BaseColor, diffuseAO.x).y; + specularAO = MultiBounceAO(material.F0, specularAO.x).y; + + lobeWeights.diffuse *= diffuseAO; + lobeWeights.specular *= specularAO; + + lobeWeights.specular *= saturate(1 - material.Curvature); + } + + // https://blog.selfshadow.com/publications/blending-in-detail/ + // geometric normal s, a base normal t and a secondary (or detail) normal u + float3 ReorientNormal(float3 u, float3 t, float3 s) + { + // Build the shortest-arc quaternion + float4 q = float4(cross(s, t), dot(s, t) + 1) / sqrt(2 * (dot(s, t) + 1)); + + // Rotate the normal + return u * (q.w * q.w - dot(q.xyz, q.xyz)) + 2 * q.xyz * dot(q.xyz, u) + 2 * q.w * cross(q.xyz, u); + } + + // for when s = (0,0,1) + float3 ReorientNormal(float3 n1, float3 n2) + { + n1 += float3(0, 0, 1); + n2 *= float3(-1, -1, 1); + + return n1 * dot(n1, n2) / n1.z - n2; + } + + float3x3 ReconstructTBN(float3 worldPos, float3 worldNormal, float2 uv) + { + float3 dFdx = ddx(worldPos); + float3 dFdy = ddy(worldPos); + float2 dUVdx = ddx(uv); + float2 dUVdy = ddy(uv); + float3 tangent = normalize(dFdx * dUVdy.y - dFdy * dUVdx.y); + float3 bitangent = normalize(dFdy * dUVdx.x - dFdx * dUVdy.x); + tangent = normalize(tangent - worldNormal * dot(worldNormal, tangent)); + bitangent = normalize(bitangent - worldNormal * dot(worldNormal, bitangent)); + + return float3x3(tangent, bitangent, normalize(worldNormal)); + } + + float3 CalculateNormalFromHeight(float height, float heightScale, float2 uv) + { + float dHdx = ddx(height); + float dHdy = ddy(height); + float2 dUVdx = ddx(uv); + float2 dUVdy = ddy(uv); + + float det = dUVdx.x * dUVdy.y - dUVdx.y * dUVdy.x; + if (det == 0.0f) { + return float3(0, 0, 1); // Avoid division by zero + } + + float dHdx_Tex = (dHdx * dUVdy.y - dHdy * dUVdx.y) / det; + float dHdy_Tex = (dHdy * dUVdx.x - dHdx * dUVdy.x) / det; + float3 normal = float3(-dHdx_Tex, -dHdy_Tex, 0); + return normal * heightScale + float3(0, 0, 1); + } + + float FBM(float2 uv, float base_scale, int octaves, float lacunarity, float persistence, float z_offset_multiplier) + { + float total = 0.0; + float frequency = base_scale; + float amplitude = 1.0; + float max_amplitude = 0.0; + for (int i = 0; i < octaves; i++) { + total += amplitude * (Random::perlinNoise(float3(uv * frequency, (float)i * z_offset_multiplier)) + 1.0) * 0.5; + + max_amplitude += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + if (max_amplitude > 0.0) { + return total / max_amplitude; + } + return 0.0; + } + + float PerlinNoise(float2 uv, float scale, float lacunarity, float persistence, float strength) + { + if (strength <= 0.001f) { + return 0.0f; + } + if (strength >= 0.999f) { + return 1.0f; + } + int octaves = 5; + float z_offset_multiplier = 7.375f; + + float noise_value = FBM(uv, scale, octaves, lacunarity, persistence, z_offset_multiplier); + + float dynamic_threshold = 1.0f - strength; + + float sweat_intensity = saturate((noise_value - dynamic_threshold) / strength); + + sweat_intensity = pow(sweat_intensity, 1.5f); + + if (strength > 0.8f) { + sweat_intensity = sweat_intensity * saturate(0.99f - (strength - 0.8f) * 5.0f) + (strength - 0.8f) * 5.0f; + } + return pow(sweat_intensity, 0.1f); + } +#endif + + float2 GetWetness(float z, float3 modelNormal) + { + if (skinPerGeometry.x == 0.f && skinPerGeometry.y == 0.f) + return 0.f; + + float waterWet = 0.0f; + float waterLevel = skinPerGeometry.z + skinPerGeometry.w; + + waterWet = skinPerGeometry.y * (1 - smoothstep(waterLevel - 2.5f, waterLevel + 2.5f, z)); + + float sweatWet = skinPerGeometry.x; +#if !defined(SKIN) + sweatWet *= 1.0f - saturate(dot(modelNormal, float3(0, 0, 1))); +#endif + return float2(sweatWet, waterWet); + } +} + +#endif // __SKIN_HLSLI__ diff --git a/features/Skin/Shaders/Skin/skin_detail_n.dds b/features/Skin/Shaders/Skin/skin_detail_n.dds new file mode 100644 index 0000000000000000000000000000000000000000..3293c36ef3cc9a70ee873a802b09c7bf1f81a94a GIT binary patch literal 1398276 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

Q$GnNRc6(<#=0`GpDwkFNP^)A; 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℞n&H!oKb{7E_o1Mw;Oi=vKhW)-hXg)vv!ZrVk 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 P`o3gMTb70S(p)QXZ_ zjNul9&x@(ug)%wm?-u*50ek}Rj5CLA={N7LP!0Rxhx|uPe)cBYFXT;X)4B=qzTN-f zx6RkOMV`s%-`DJ>UT&s2wdRpZ76Se`%sv?2(R2LYTf;N_KGbgu`~=l6Fy``Y5{53mS-oI&qz=wXr*OaCPkADvwb{K{dNcQa`d^RY5ICh`ZY=NKgkr$y?# zQx;dW>_Fj`zfC>H^ZuHg2qAFS1sFm7gTuaN-)U{y-sE=fqfX|2lpX4S)28W_D|b<= zCG~;;(9fbht;@C5)r`0KA7A6igS*y=4-vmYD^5*wh}>n2J=H5v*JzgcH(~tV+;r=8>W;4s0?f!Im zz6J7yX`=vUmWaMirzIM91K}Mt{u=5!6|Z<_KJ{Ed^@WhgZ!|UO+Zi)!MkCOAf_{@r zxp_`&^%>(h#D5K;F?4UlSP!Ou%xy|p;kWPiBl;I1p9~4~~D?t|J zHh&7PFgWQfOLX6>{0_I1^dG1#l@c-v51ce{U147)n8NC8I)`x*eGArQBJPF)M=rP? z;(NA0?2qrHOD(?zdOHwL!Mu-hq?g6DKY#1vSu5bH))s_ks&b>ViO0k~@sIx3pQv!{ zpRC=m|HVz1uEu8dl`33T$TEoEApYMnS*@VRu|LAfcQc zYl*bl)~Po8&kZVSDFV`y>?p773_cKhtMVA(hsl&`+F*oz9{fFg1t=_Too#glg#l} zK9G-v~>at?A9*cb1+GZFK9%1N|dW zT}#hb;3EN^tvUFQr`ccW56v?sfbTO_+c$#hxsBWH}wY=q5FnKPYfjBKu;<=G#S=wtS0YNgtYN%m8Mhm z7bN&&L1gB!pyO{Axr9H=v;cB2n{+vgQ(E@RX{w|j5({oAd=j7@TYl$aSd;DZA0jGe~$5zw2Ye4cX* z;v2v>BJKZdVMOguN=W*O&U205;m#XL7BXSZvp&!tW|Cag1p3e7P@Uyt#|uXyOS4~o zk{`mv7}W;Nj|BZj56ebI+kudE`O;iA3U9l(2-@FzU|>iI}w56}LufycuVM z>ILloBkIcIq29kYilXG+7VU*l2BRxW$kJ|RF4=~V)UAZlLP%ny5<-$CB)ga~NQ~X6 zZZalWM|QVTQOVW}V;Y~|dC$Gy*Y8iSN;99&`+d%Hp7WgN_)_x=GM8l$R0RDZzx}){ zYPF16yv4TO`S7`nO8epf@OPi(H;wk4hk9(7VjwFPOy=jT=yu@W_Xhs2)Ulmi=A%{udEuh&XtPw4|c1pNvg zN1VDePSS!$NBdQwpP64cFrexsJADMz{{l`C5!1125Q}%8NB^IZp#{5Kqa#Op;azve z%N}f)S&>{+Sk{wAEnW=Ih*voFQ_?Vo0LOK!N&m5*|NmEZab|PkhG%V^j-}Zb71Ddq&PE_fgFrRPG zXs@1q&P6L(VXq?SZzd?YF*e>dpX8!!4#lAJVRYD@Hf9}u$oo;;q)1QemxohlZDlQt z%bB5g58qee+TgA90iW6JX#dG@>w4h71SZc1EAtS4#tduBS64q=WY2KM+9F_{qsrGd z`&QqM=-XFwcOrb+C}mh(|M}^LUAr!d^x=#wbxxG(AHk=~y>_g&HaM!gZsgOp12peMZ4>G*nF@8 z`-V8WFEySgPZG+95>r+Jh;vLWjhS;Qizwsz)$3SFl;w8rsE*5OkQK{z_-V^#di*67N z{N?owo1iPmFPCp1+cZgDLW|Y*^%3R&2tt2h*zt>T-xps%d{9mD_+?Jv-#aYNOiT7O zWtHX^UyVX|(>Uc(B77e3qr{V465UMeH4fgujePbU;d%vQfMdol7P6>5af&v7*4+?R6*%jG^pLR6 z!kf*0-B*P1lN$bFIl}%Zie&L6rz|pAsb@5<_QTHgfZwNrS!`i;kG_|MLuY&!>?^Gc zZOEvDdh)9ycE{SD%(+RFwM>HZ3-yX`XQ|Sc-4W)4HKOOAVDI~KS!&XCmd44R(HQI0 zi|btwej(22+Q<5}{D=RWrMwFIlSFv`8Y_@|WKb%0Q9$%@#gycYvk^k3V4G(K%s?A?}5%S5bkw0~tX8D%Z=;g8JMe!gkET-A~ z?hA!I`x60=&Ivqjzyfpm-Ji%qEr6GLDZn?gp2d}Vm7{%eH0Y1&{&D%-*|e1 z*)O4N#`Cb6hImgkuCAdiD)sXB@M-8NK7;0Isq}ZkdCIWFtn%MYzoyPw^AA=aDl zU;P@nQd(MR6!caiJfzZhX&Ac}iR2W z@<${0rqP>h2SGnXG3Bp7`#2~8nL~y9w*Mjjz4HR-KY$;@9TRdhxAHtSDRVfNE5 z`&C>B9j9c6%$0*`-P`Yw76jbdv9@PE2wwy_s@Yt5w|;ZX_YO_;`31iA)+*bNh~07& zc(s*4JWB~GFVv`=S>`P!`TzaSL|jC(3?K2MOh*d^GWPyK3!%~rz5hURcTU*j@cjFO zpH-lKdzoFxzlHd~4C*u=<B3>?$F^sm z>$x%rc=Mm~T9X|*xqHY*9+rLL3j_Q0^mdq?`ZF2LW1K&=Yi7}#o#~?aBUKw)U*F!w z`J%x%;IA*0-+S=bBjbix)<=)4=J05_|XQCCN* zDMa{SK!HMm)jfFs31J&>8C2sTvQ!9)XEiCAzP^D3{CHm_4(WTagEpNr zo$Fao&wPXW82sORGHsqcJG-?M45L9W0(zuI8sS-ZocX51!y>=@ywFJE;BtJXOnwve zbHJa(Ug*E=ug%j+vezn=eHnjso%slQQ)YD2U@*#`nEUO*Qt#~XgN!x`=vy=jF#pH` zYFA4^Ww|fh?;cafS4|~`@}(`?J^DoZdgxb4un(uk`@t2^OH_Qp$|dqgzeOLqb%+Oi z6wEW{mm7An#COJ>XJe>;Zkj}76%ZEfi(>@B=cm2Hu2z+5;9oRVm$_nq-zG5otgfFk znOgIClytMRfajsUP==s4u6zx5a2HN6$ z1E0bD7-cq%@83_0HScCzoJIGWvR%r?ca~f?=Xn?U!!JKqIjJd#h1@gAv@Jb=>UTjS zUZbma;80(2>}l{{WSq7=D+BXNwPQZEOL`__8je{!ygMcc-1EzIK}z&_JjDMj*6n1R zAMDr$-~Y1_vA{$^Z0PN$)kO`6uLM1}x~A{%YfIP3Rf+1Ih^bw43D4*1#iwl`pZ5ka zfPYrM3e>p)p8@8kDSI6&LGZ#rWZ2cY%{t%SJi!%m{ zMIO6Qe651%WCia#HKU|IaiaUsSoEzRBkE9TMU7ajPy2sI*!%a zUpl0$l{WdznrFK38Y0B|pbAwM#J}PmtWnRC$d+W$vAM*Cz7kdR{weAjS2tzX}Lf2r+4*I!t$~T<s);KFGIB!nTVmqprD-iriaj|in|2|KMYF`lP0m9#=q}y*@$((K(#s?DM z^CsxWU;fsze;?Qx_QUTVHFYPBoqQSY7N<{ehWyMkRmIbyKpDK94XcK!xG`~y7eD!+<1SZZshb?b(zt>$)DG{{rWNSG<%6j zhL}>idt913=rv}Z$v)H{Rz>p}{A4=yMd1F&BkOD8U>>QJc}@0tXW@vt=gYp5NekRO zwlE{aTw(vhNF#6ZLMil17b*DYKdPL%@=D}~X_TTCew?#D*K1(+Cy3Wvfn!0rUYEja z%77uk>Kw#-hCQogIR5YGQrXAAul3e>zW?%GyyR}pSEJrZi=uhh?+bnvTMXv)R1WM` zzVq7M-VOL!@LN5Z{w938KRs>N(Pi_qvqHs-!`=CFDF>5_Sw09)yxVNM0_7J$_@I+j z)55rgZK(p>_faA4{SqlD;`Z2=wWG1t#=L+1TNX~>;!D-gpEDzj^FPp5-RpXeZ$k0M zC`lwVDtBL@0f;vP4nYqaND@aH|u z+qmXql-`)9zpK;X{ITlF^YUV9hvefP8qY1&JyUwRUF9y~i)3@xLOlt35_=rU2U};v zD`PuPS{m}1u&v3*cwgfAo`+L_CwamOzfze+k2LvhDe(KCUM43-#U$D(xBn(5gnR{f zD(hH~D`6{VW7TaG?~TtA$HvsJN+(LFm6)Bfa71|F#Hn4pQ?0))LcQI?JlM>(Ag#4O z)9Q8!@(+*V@WX8Hw^`$q2RYz3g}q6}*z_WBNRQ7MNr3Ou$21_%(DL`J)%s%z^aEht zBk6@g0f7{Hpu!^N=mGM6a+jVC2jZu9U6IDRM)-mOkVuZb_Y?6! zSdx@Z*R}L}JhC2@Adq5LgR_x)^0DUyy%uHxjQzx~5U{+ul@YAhKKM5N?vAOB|yA$yL z0l#szDzRv_lu@c$`7p}Z)>egf5pfqA%6JFO#IzKh*!LHUlKyJD#X zKB~;Cd>G>6vtH(%yq0Y@pLh+=F7`Njz{`+q?H>>LqsFq8MlM#^a3l`yb1V1_N-&PCH*nACTKn&Ox+V@6 zowQnEVuzcVL=@*CMHw(I42-mZ+& zVXPRF^JbZoxMZ;fYKGB!h%k55s8k(n?Kvwegnz4Et)QA^zmG_}VcdZJe+mQUwYZP! z=F^z#5q`m(Ze(N>{Ijuo@K^!VFEDQ#T4*<@kj{UYW&!m9_&HtPudZ9zzwo0-w9iS9 zV6PivBb$F-z5EF9x2;SfxjYdM{-L>AHR$~X5F+%3?ebdqY~(Kl{e)X6|D4@r_$_k- zy#G;(-di?{CELbeVEqse#+VEVoZ;i+bwQrbmC(QE-_mO$(w?+DT9gj_J(>sOdGCn} z*S4AQgZruu_-~p+7PjMxHl#y*g87H=s_I%j`Dag0(dXNqg1*85m+w6zl=4}ZTnP0D z`15?J!rKi6e;LWiLG^P@Sqyao+vNPmS(|;~)KEqa&3)MroR^cyTt39Z$~xls#|dVq z+C%o9=S!DAU=$V=AboxC`>*GZA^syUx0%#ww=c`gzIn)MTsRSQZJhR1AXma&K>KZ& zKLg#oH5W<9%Z{%!E)wx`e)x__j&y%)-c?b*$H;HtnH2r6yT8=rr&JK0FwFzLjdfdZJ~vAY>CXc_ zHEo7V9KdK;C)YXUk9d4xp1hvJr zEaHFrcF<;r!%sK8&HVe@)Tj%?f|9KQe!7_1nK$b}FNxu}3ayic%?_2Lk*~7o>;8oP zYH)_lAjJymO{3fk`$^@e4Te7B6!N$GSD$YCe3u9H)8%wQeZvXjQl!^>o<8X@>GpC_ za#JsDjD{lZhWdYQOa37vf3)E<<&FnEjzbGFkTP%_v2!7zuQ~e3_I~M{b&JyeeQzTm( z;uhF12KvZ*5+O|&^$tcB_O8=$;hgMT19~=LT^Orh5Aiz!GLJ-$Ja(r&Ln9LS9bVl- zE?rVi?~=qoh(#Op^C%8@4f61aYv4yApBHARq14uB}ii9 zDLH^AxO_(!=X19|zED29Pj1qyUi7{*#v)dGbrfv@y*22~eT-K~{JkPY=j;W*o1+we zs(E%cp_{|>!pt=Pj1c)j+9Lw7>vfedXtm>*j@f!R!shF1iZ>N8`B%=DwphkR% zmSOK&~F9)Hd#78CabZiE9+Yl zJRh#H!&mD+)V4CCJ032+YCtRctU0Pa4fBu&iYHQbRJs@C6^?lV|GmJP*%PDMr{Xst zdEwUr_H=x!i5T2p@KbQwD}Lbh$@R(2YL)1H8#@r$3N(w&Z}yo$JOw=Fi_hNA8YtKE z(cTx{6W98rDBlkF>>$=me;MFQ@N-llsO@Qf)bo!)++0)Qi!86NQ9rP`>tJ#l0QCjW z=WoL8fbMY+dQ2#p&a{4B+p%LPi!Yd+%A(J{;jM z6wicZP(P2o+#U7!>bV^{Cl5NTa_HiMpW}zNY6jx7>-_Bv%Lz``yxExr1tzH`~7R#o2ua=K1K@H={^2CfdTvh`1RH2 z(Z`^lJ9|CToEs$C7iCD_q5bofCcpo?7mEMFfZN;H)mzjqAMF+Kp#en6v1f%V@-7Ry zEJ0t-4bTtG-?r_EtEG9ovgrK2K7Re$H3=J)oM~J`Vo4rS#Nez{Y#egwvy+;oe5g-C znmPvN^ScpX;7HncTRL4Z*{*R%QbKL{^HSP zQUBDl<(Z6n`pWR1!*Pb6RsBX@y97@N+Wi@4H}<>Z0^ceWOU6%41=Wc&jxUD$$gMHR zWz+A;XxfAITq`ob?zb)=UP`_xt(Y_<#oDz5O1!?;C zNgvRUrOQcV6HNW(j_Zs(w7X1jShd!d^HEsiWL$xp{1cDvbH;SVP-XkVtR(O|haI|f z?6_aBDyhpAP@lp+>x~vKv$O9nKL3DMWCi%#R2BRtIwKCn444m3p2NfQ0KIl#-Vw^4 z%V!aP5#YGyIHydt|C1BUC%}C-)dT!8yK!p=E_!;m-Wl@NR!$kjD}G+qgR6kAzfknD z@~K}X)5oJM=?Jd|>RFEMr9IXUSY-631o$h8c~!M`Q~1($h4n*irGSsOhaKl$U!HSd zW#I^_f58t?V7sU(dZ2 z_n3tW^H-nQf7I^NDR@4$OMFZ%8v?@U0HH`NyKlPZVzj^#JatCN-@1ZL_EIKWb7XdRAv-t@1Vcf^=$Ww z7i!-Ffj>9`Zt1d7IF@u+lsJ1T_TvBcb=3Z)dT6Jlc~1%b3rA=zID%^7Ej*`BmI1DPbeZUqso}LtQ^!C&0WveE+?+-D}FwK6RxT9Xve0QJ5Di zvtyiq`$&0!_(qDMtRXehzVMBVRvSLG^8Vn%OxR~_7B|95*BZUz%kXGBdksDh^OYQx ziulYx;`q1hMQ8Yyg(bg-{~cQE?lt$|2Xwe%J1J_3_sR61twS*o4_L;_ z)CeBuD$2zl>(S@N5*ZonFSsF0u&_6$fSIv-1oVQPmx-kE0jD$du?P>=Rlna)K7M>9 zWh(j%^rwcIe&8<^Bq`k{eWYwM9`;!=T}@Ujj+0K(3GfEK4!E_j0^1-~{DCa?JmSj( z0(7ZnQ%7pwvvzB{7sTNoX(k_o`Jo_^?bsK@M+gfJ`q=9Jxw&ps+WG2Sipj&`f}3#u zuAi%Udlc>q?4!lMWVd{A#V-nl__3?kGF+|qbebe#5U&GLU0R5r-@)bI9s~11``vH8v*`Pa?G+O(X zAZ$br&J)Cc_x3&=ot#$h5gn-aidjP9mP@F0(uipS6QNbc zFjXSS*H+QAZ^XGql%Ff!vRLY8O-tkCZ^8X(G6p|S0e_i6^sBjWsMjZi+y4sQ@_761 z=XXuo!kZ#r&Y6p3~VZ7d<^L9>=qyQk~=PTk|9^J1zG<*e{aSa?SY7r%9V zO4=N{U;7?In%a)~M|^_r4i+s*J4cD{1N9e+$&j)o2gycE{k}B;>4O6!E<4k66v|13 zI+p+P%g`R}y0P@TnDHd!OCI0z)5~m{sy@E|xAa&Xp8X&H-P^|-Trv3o{9P&RL3xSg zQoTWr-UvTXo;2^se7>kV?yP`0njg6*X*uW#1wujd5-EwLn_wT7#whwe!Gz9h!LjPv z|LM(-(GqFns~S>lRbRmeZ!IpAK-=mOpUxy(+V~E?Sz-2ZpTCGdO|YkH>rT>y<8!YaVv`d%mVNV@N0+gWp?b=Jx%*}e20Azj6&*;WoX`x^~M|OKbVg{>{44> z@**YSRLugapP2G^x<*V)qS=GbfWN_yz@b^4wyT>Ft*J7C@Um$^0Keb%fqCRVR{bADdQ68E)VAvI=()SXHq`H6hRa^F{kH3-eUGH0NZ;jeLzZ7FzSGiKXCCT- zX69>87MrxDOZ$8v;8O=)4cV{zEir=eXYE+J2;chh>}<5nvZgMio=5kuO4pLWW@D~| zxSYkX&yZ87F%NtSKWJNX2N#}CAH~VHGm@&kWcp-HBFuM#e(Nvm_JjLN2`asiFP}A0 zDi}Il$5Wq7^+rXZ_!7j)qAo0a-6?f^+6%=?%AF#ZpSIh~eJE$}(EQ@QWb4_NV-pnP z{tXP6Phi6S!xxH*H#X77dn2vU`7uh!yQNn@aA@z7Nq_gM6rI!}HSmw;2RZoe_!G^? zn>v6Y$Iti1{XbnXsQ>5@*vkOCx=W1HNd7^D2L)l>x1t{Hi%j@puY~vxCNYyrrL}7I z?9-e?{SC?;@Al`?uN}BiuVk{ z<$MYKR*7ZE&t2lAQ4GRIl*7%F9n?J4=!PxdE2oZGJ01c5B*0rQx=M6`PnoOpi-|F* zZ!`4Tq#N7@^_1!1eYAHK5?UcE|B`4JD78oZoAad7}jLND~wou4s30IDx6KkiYgYGvG$EolCOt!UzNE8Q6c5MD-xW4RkwE z-k)pJgwJ~@2G*;+-wgfL{x-;Gf?S?vDD|)3RL@UJX#dk4F~W&}damOc(l8$MR_SnI|wT@SLxaEfDOBe69>Z4J&`#f$A}lS@SX)^YK& z3vDzw-{vEKH2;pC5Bwei8cT7uo%RC>Q*P4hMGY)jK>hkEd*WvTLGYzoFys?O7zVMkM z06wZP9J_Y|r&&H^7%(c8;5uy?3elKNItgo02%go zfxa2=>4jcHJ<&d1T~89(9D;8*FJWnk1I%pcsPD%_DiyqjVnwRHIS)76*1(D;0z@VLdQ-vRH; zF}F`MwRV=6t%wVS^Z0re(`#&L5%#8WHdI|+y6IlW#|sVvM^F!FV z#>X(p5AY?-qh_%PV`5T-j>gBpcVouMy#soBwaz}{tIc*o{xo(Lj=j&kS@h4U%7a#b z_e|ZukA$$@ad^T^Il~;P=`Xfn^sXX_`*i0N`2V?e;4emA_O`yT?ID`S1-%FCYrK7D zL?w8iHtPS^*%zs6=n?D9;!ELv8#Tke5LS!4D(h+B4e`l1;GgadBfo>XitWfhOXL@m zS@4`(;&uKTLn*X_WfjIuImE(G2G6H{1cUvyFpLhYU*UMO?U80)%mSma&2u8Z}q7i+;_rdkF6Z< zqk2@Rn$^W2dzIrZOiN92EkK{PZLF>KvEK8@txV{53^UzZ3i7ko-D~;11@I{w`uBPG z!qPB5cYOC@#D|%l-O^=WIFzBb(NQ4k_xNS0mk-5j+*qj^0s7H-q$g2y$|d{H&ki>& z_C4ZA)<^YyjS{A(A;M3NEEBC3vTWTy(*BTd`KD);I`>2!-FW*xd1l1C4?9C5Vu13yfx&KdkJ5T9j~htDL?M(W~@9lh1& z0sV$Z&Bd)`XP1DBS5AdUA8UIh>GB1hH?zOKyAYj6%G!>?&Y4Z|9-LovRF4HZ`4R{u zNovMQjrS;i1ljv-6IbVGa>;g*yOqhcjy8>{+=-z2S5tvouJ*>XXP!R_Swk2!?}O%v zUvmYK1^G?SY$Uz&lPe$|!8{WA+{^4cS3M`nWIK^FY47uP9iQ8*gwM?vMfxQ_o0)U!M!CMr4B+{IevmjecWoggeeS~>JbeCEVL5zS zwJipvXTGh4eTfGhIj@kOpl(W2J;b+R?4hEAO!&oT*JjkW z=7S%PgY75y`&iABv0A`Kv@euOFLr61c}F`P!z#ar96CAvWD^ggVAQTzSLDxcl$3?j zVftu?(GmFYAdt zR$9Q%o};hEYxI!%RoQVFH6O7FtU?-BIQAI7770)DpEmv*dff0d(5F!a@C&i@Vt06kz6}0J>j%UsdQ%QyNx$Z_q;T*VJ>cN$FHgPG$R|=it^P%=_}XiE%;Zf$`l~p^-|P69eBlhUjN-WQ~~Lug-y{q`uOk|A+y2* z@Zc!$N1YZHW+vDig%X?KO8&|=pWGc4w`a-<_f*sm7M{!*Q{dwbY#BW#m&3nA0?UYOq-}q06 z%;_TpsdrzP@crb@4MIH%e7ii+CkT^f2WZWDn~%oz<`pF)y~y=#EbAb+4`Y;6dUi(0 zWJKyygFaFHA28NgCJ}NkaKRh;`@2}+U!_aYOw4@RZ&HBb4JC!XcPxmz*7m`n^Qd1g zNU&jx(*}~>5OWqERXKCexzm30f*`1V@m`FHz7M5fOr3K{_Aj0v+YiP6nkUfj+iNR5 zzCN{Zhh6HPPMe2aoNvNF+j>R`;8*ao$hzUp-LR3DLpMS3j8ZiwEMKhpaxKpWhwe`R zgSeMn-naJQ^G$CW{-TNeHJ0d!?K<%*k=3;NjEQ5HpB>U)eS`i}$n(3l+G+@`V^|i6 zbLFW1nR+)X{Xfz9yT8|B7S79OzN73Gg^UlZww0R*B0e^P+d&Jl|64lE`+_L{2dGiI zhJO5`Iow?xd->IW`qtIdw!YUsi0`D>Q`sC}f=*m^aWBNj-XKGIQ8Dht#)tROeoUA* z2~g?m?)p0>+u@C#|S{pd?sw|iNo#nyb z`9{R2x+$rtcScANemCI3eTRMREhIugOn&7*sZ6MEcm}z7Q5{!Wc`Csrf1rG1RDl=D z-?CIVxbn-99cz3oj@vmbhy9E7)B3q{===uyhu)CUma7k0-l&M`{Tk}4Fi((cKTSB`X>CMSOg(Z1EA_HBG#g>(8$nO?gKZ?bjAur?PFX$b5L>_|JCF z`iYrIxvhp7i>xdcV83c!6Y<<#3HNO+3#dNB{CLM>l+L!s*o8%j>ZK>4`BqbG^Qe1Q zBhA0(<^Ip#-`6Sae1q%vcc5R#tPAG~i^)fopni_(!IBOZh5X5-<&6B4Q0a?E>$zXY z6uh)}kuTgMI{yZmR=>!HwTi}nT0s7R=V3{s#ypnyxXU{U{(kJdA6H?@U?%9rIgYQ0 zU!&OWS+V%VGXENs@1bAqAsox5eT?oAPisK@0v18;w3X?K7u&aCs0resldb+OvZ~vM!sAscu(*s66-5vM~LGa$0U=vaOqKEI8eoolhGTsOHT>q=7 zXSq2$Iwh@Ef*y6W>-yhmJI8c>2!eyDE2WO@-W_C;q_gfi+E1XIA4mrN2kI|iC&?~3 z^8L@sMf={)epS@e?AsHv+WM} z8?FzzvE=yXwAeez@Od!i;XGUu=}ZBA=VmyckiQNu=#U5&a^-5}=IH(l`-Eeo1&3Qk zQ~Rtfl^xD}AZMAh3Lcq^G{jIu`niDHI8ObaVL#oIy?}3>FtyL35;@idpX?Dyy9hx`4`T`_4Gtqq(!b3@$Zc@ z6@Wh~dNHadiIY3Oc^C*sq<{R^-!3u3ths&=_@9XYLV>!3SiZyXCqr-K--IO;6*_tJ zuDkzwJ`Bz;@DrA01Mg;SHwJN_o&i5+29>6+KG?>n0DnYZ(0343=uX!^MGY!i5K%qK zwC4@l656}t@cusF*UX@5<*TE9RKTUl`3xF#^0B0o9sSPM2@4xw-tRRJ%V+7*l|C8R zrms99!lQl?T#8sv4A-f^H^UU@u`eWswcVcG^T|%)s2&bTP*vxcG~!RL+yQtW&J&rD zm6O=8)H30;rwD)O;|n!7@7mT(OiLnv-nzr~1+)r@ttHPNp8tP;{dKo5`0%|&^Jx)5 z)OnMT%FCZ}zN7jf$X_X1f^}r&>FfL?I1gOGdQFS; z>)S5E`&WHIk1tEPzxj9RoRw+rkGB#M7g`1K+NW7bn*jgu0#mG?$z=VJxr=L4gg&pp zN%!*1pZjYQMn0%Q|6z<0;rlXM{da?fO|2eCkB%kU&)BjabkFCmR5r`Mlq`Vwpuh8` zZK18q%+h~CbJxc{6iflZF&GdyFUmLh9CO0+?5YLlQ02)Yy?^)3KJSdDZ=i2>S9aO& zY^_%Y__mH1FpP)y@tR5-#UVcM25}H`OWf|4A#Gnf0sWjFG2UN@52O?n^pz#Xx>f{( zU+7#`H5EU07rj3M%Lno&)Nh968jRxxYyCGmBL8u_&%-wpb)7iszK*0+qz4RSsRirj zx_*!(uO1cgL-x3A`nx2zchk{jleR1#W#QY{RNx;^d`3pp(D|+@$E%m?{f!Ti$gx28 zF|c~vx*XrEt+nu*mKDTz0mE%SyTeNP;J{oAJg;U7gXg1r>+vDO`V(hF{R;xUFh=La z?x-(w{tc(UiT0N*=1LYy9J_Q3#re&ls9NG_C09^y%zS z?MM}@0(=2{{@5I~XrFQOMNiN0H7^kq>2;y;-VZ!`1;MzqxFdK3+BP&8f~y6pN6&O7M$sU#oWPw`Pdy>RBg<(l@ty?S#B3;abQMFxoWp9gT1 z!&X~W6sCWtA-up$<|OKI@6^Yis(|xNnx*LTY_*rCmyM@NFGcvAl6S$UF8+9>ym0aY z!asqzIH;co4oPo46odXfg-D;sr1h?^b~cJe`>N}bR!HFLC9>}8@#D;)e=@ogcNgBr zbuh)XZL^2_<`J=T$gWq7@u|_m$_t*Szx}l5pP^2?A2|e+kBqNHm+9dpvWD;m7tndA zan#hHP7mHoSa%2TtONKJ7b%b(*58Y|p%;PjT^)h!eA(m1#lAc<@uhQ-E!i5qWl+Ba z{Y9wTGFKV5Cs}j71?ba)9>wD~n)jmljjtcL*v$2NW1bsDeh7{QS@Op=efg8o;F_PG zQUrb%G<4osKEAbxPn=DVms*Ysi}zCI_oL5a>}GEpq2;yN7dqzkB zJpCn*q_)Elm$7vrwT08ulTw?hh5R~x1wUu;L9cRzTNBZ1B~zHFEGeVJ7x7_+9v&o` zqT({WsZ>-y)g?<7>gdHMdYPL6KLh?Zhr=SHBaeIaW3po5x_7L}9l$4pf6t59glgz# z{S3^r^ypMdP(9dW4)?L9k7-AQ{gAoF{mr1Je!l^{`NF z{2MJR>Xul<=&&f>80JPE?=s&0c%V`my&pm3ZE9iPmhFV_qnMQ()CVa-S_pMWO4%!@ zf2r~vyR)6D1KinPbx}94t>MqT^-f+`dSzNDG~KjdvcwATd0l`-r(RU(EltX63lx8a zO=<-TjGx*Ue_UCSuj10`Y5fKK0r+)Kdmg@l@6$^0wf9|{(N}cY_Es#KM=&7nW!gx_EF60@^TbkYlyIU!nMDa z$K}cF1bz*>t$NpvI~{L`~Um7j31|5E(v&*Mai|dwjOVB zb9Zq2s|5HZY`eTdgxV4vkJBE4d{r-!dxzdF-h_YM_O^txCdm&YQmBVQlg4~o+m%3qda^670Mt#Pr>37@dXz!#EOG`ZaeV4p{)J@9G5 zn(d);`ernX^#agSIN4nDJvJ>l5BR&;^tPp~HT$J}luMJX$X~LjijR|#S5SE#b3P>b z!Fbi!zkZr`LSuha&c#*di{ioTbwxJUAMw|Lo+_+)^8OVCsU4akdl=x1hkM{Od-alo;6Z!c@&|i8yU7=c%oa+PnVix7v z_|0fsTiy4xZcE_PTd_MUG&HI|!?FkGSo1mR%R()K^AOLjhh(xncf!6brl(i-F3vxL zth_EY(RuyKo#_l+T}F7)Ak(xs^`+ZR&~FI@CS$gG5{M5lj-0aH#=ZhmM{ zvZlY?v|!G-uc6^rn=Is`N(mYJz**zrhpka)9`E7BuOfe-R-bMI(4S+*j&=1i62D#Q zhj?zL(z+DY+x^(^wQIWMohV+`3H%Don+)ADTa5AtzZ|bo$QU|`%_;+5 zWXlWmylw7td4JR#!=g`=KRq;aW6Hz`QC>a3w>%qz_@tDr-mYHN-*jeO5B2cZod!tX z5zKBMb4U45_;9Mjrqf1AQk9j8e#Xx7NNOva9<{hI=}X)Fs&jVXWTxTozn8ooh4^tn zE|p4ce%JW4o-S*4=1>==bW&zpLWI3KW+3bSfTh~c)PHuP$G`6 zoJv6XMPMM#&{5Z~@*-+{A633Xa_st-AIu&s!ohv-WqNX1>K3ziIfV6>+}BYm)4Lw- zTHs#|&fS&g3iuuRQ|)BBO|-ItBlZBr3nLGf?Y-4KRgX#1B_h8JT_R~PiCp|0_Qj%k zdV$*tTu1lTQt1ajcT9TeiROpKoR8J<%YC*r*-mf?rO0NKW3MbkNl=6dU+k>+0=UrllQQU{fIm2H|QW9ap118UkIOzdz*t-^MZVK z{f>EO&?Kh?{1-d5^_JgyJTPYr{CEZUgV1qo%@4_G_wUXvh56tdO*InAr>r2^CRE=U z7!+hAXr_oV76kelOj(uZBr1oyF+==%2`rC72?A}#c zB!`VWRB^FzY`6b6|5>@X#Ku;S{fh^~<)fWgfhfNUuM&Eyb+WXGcAy6@gXisAPkbpj zuU^mNDOy2)jbh(OVv~l8)%I!w|3Kn_|MoWZyG@>gK|>Glk8EW|u#RY@b$xbM?LI7V ze0NsQyPyT;?6ZJ$%7p&9ChGGnO%~t-VQ>q}e0nEF`ch*cy~X5fUqw~dvFE@KPJlm$ z(v0pz)wTZcXi+^YZ?8sgWxpBPtS`Q9c`By>pVB4T*DFr*M)@ruPnG(sGuTYxd@117 z?aeSB?m_Bg7!Rj0#jJGT&z)sqpebQxNbE$lRX6->K-Slkm7eq;IdlQ(rznJIlP=uW z9o`=dcz-GRUp~D%t9}AADRUW)8i)7~`n@6({Lhv2YzefVTEizieCnPhi*pTmT%6r$y$o) zMFW_RW!MZi(ival_Fi(i|h8~i*AkFI52a3`dyf1E0!ITY&-WPN+ zRYGm|@@y(qvjEO7KPb~92=p?rpPZbhrbgNu;-$#0ho76lU*2PX_ITL+Rjd~9V_@b? z>}Xj4yr)jf%si^^Rqbi>g|;A=tvyre&&mH0Rg!jOJxnWIt^dRNUMj+46gQ<^uH}rq zu4Sxtq^CDcl8`9;by1ynR15SPz!$jTX!NMJiHjFiK8AgEj>r#Lu`sv!*t|`v`I=pn2vnVfACh^*$cw(#HP2~sg zf_Qv{TSZVDn{Iov+;^nR0`;#=552g#jP|3=kY2+BJP!FwhS)+mOYgpMkD(GI`|Xxw~?4noTZ#_pdQQa6O^HW5e#%oP z)3xkUWwUL%v$dd~fc>Yw4$V|oQN5nW8`Rr%B0aIU@-)Bi<|8jj_vrL ze+F@}7n3d8kI8g`{q2QH`ltV%ivm0`9CR&$I#HZH-QO>-4EzVT#zVZNv-J4t@{Ed$ zBK@(Rghq$=UggBo!#u!C6Gn+r5)z@(8D7ec6m)+A9W`ajf{i7Q^ttcNhka_!Tu7sa9FOiR*xCp8YmDh8Wk=?;YeutlX znJG8yxb!~t6c9s(kHxX68e=jj-bST-+p9DZW^uv|>2nvYRDQm8Pi!pfKfOxZfuxHp zi_z7$4<2v3<17>g)&3(`Bqs7-yC#WuASc*KpRw6)HuQZ7JTHpajNZlAMFwKX(~Z9xpLRe=evdkXtcV`!Lpu z5~N2q^$6lf;F`nvnWhWj{7Ci_yyqgs4~Qjv{R;JADeMyhKbqO=<;25JLRubJ9pZn- zegU4mk;%OY=anaLdgXE1Hewn1euviFXxzbc{Q4lMzpmj3oep|#D#!oIPh1$9xF)^! z@=9g%<+-|J7o1Xl2>r24wvRsWW5d|`+{{dBeAZ%#Oa11$z8!94-Q~QAU$wFg{9=eN zp9Qy-qUDcElol3`I3Rw@Akm&o%NS0Xa!g&b^dFl;12L)tQ&W`squ&lGLI0Om^_p0a zsrVp0-#i8S`!&6$uk2wNV|$|e!7E4)F~RhI(azzJ79Wgr6ZsDXepx{!a&O+$IeNzz z&C>`rh>PDKf6-Rk8^)g-4Lz8yO>~|T2!DMq5Q~Yu-_HGa9yd-&TKey)9_4_&4)IO9 zU|zuJ+nkGRZ0Y~b>u>M+RVpCgALa>ii*Cg{>buzIx=&&h{9n_SC6;}f#e%acsJPvu zS$h&R*2lL1K3mJZum|`$E8avWnWuSI?4N(S;;rt(=XH~kiCOU*%`5KEa8G_#>4%+E zD!dQys|nIY&(GB9Q$Ybu%fL%!vLE^H3D4MA3mvu-1ges!As)hfs|SIG+fwyiOj#`- z{3*L*4Wp1B1n==n1;D4Rrh0E_3%am;bKE#OqoE&bWjcou3Nj`=H<$^+K_9v?M$H-g zz^?r%jFd+DPzq7aVYtg~$6l{9!&b}T|M#=x3_30fQSRqK{R?`AM-0iGy#qU$o?yKJiKBqoItNJ_O?`!$Q78le{HZan~nFqZx z`o3|v2{|Ws63Y$FOwf}yf#2Y%Y}u1FwaZ&G5uOd& zuButfTb7|f`<{U?ok5oZSr{|dVl=Jx8FJ=A1f1XJQj-PBM6d2an%|!ORFKWjWuZ_OX?z98Y7hu5u#E^*=7t9Bg7~rW5_zzqPi_3$vR`1#`kyL zx%c;{4`aT|dC&5k=R61I39wpY2f_~Uzp#&U%sqGz!U{B;|bx9d%g(&KeWDhgN`s)r2m1QtzCEJtgx)l$GGyrgCBIC|8G zksRlY_QTBY+M%zlhxByvl+nCQcyIu|jP^zDX{_W!z@z#}RY9$|ZSm19m0G}mIm|Ki zQGZ738Ax5skUaqP>9<{Xq8uHMZL|^9?>?P$g!W&4-dFwfaqWTtFY$g^P;j~7=0h)D z3Vi-sRlZM?y1I?9S0oMc9ned?Lyo#ed#{g|od`$@?EeoxamW#hc@F;1&(y);@D^nM z4dInb``9rDhjkftU_XWp985z{Uzt-ShHO6>E;8>fMg1ScbZw4&jzqnpN^c^w8=EYQ-4vhcKT`1_G|CGp~Q=GPFOd>XKGxx7~*2if4dN3GuH=B3I(L z{bc&XW6y;VUJxEmH6P(fsd}x;wL^F-77O~o$D;azf6aBG{M^v<_`XLsf7X0(IQB0d zMR-THe=lU6tUW&r^>$f-=7=wf@0(}d*P-hn28%`&(>IRL3=SV?-wyHC>AE=E^=++!xg+hH+G1e`nK;ciU5!6$srp zdb;@gim7>Qk@v~{PxQPDtG`-G>dG#6v(CRQxV~I&kbIPraJ(}X$kq!pb(Ot=;!zEg zsD<)_UMxhEVqqby=vT=bM)f`9vt;YZ_Ue|#-+sCs0zbq2vJd}9 zcBFmXBiC{P{$co51gA-ow>Ooy-WMvCyRZM~f-B;O*lzpP4}99AuC}?`5kF^Tcdw?& zC(FBtzva)zE)jxk34;8~!Q7&2nsFfc+y{g&^&4w2GzkVw?VlnK%LVnaBiWp}8J`#P zg=>rOz?$?N^+J*3+9$0=JpjM`Ht(}cRHDF0JP^O@C+N9w|Bb)hS|Pt2HW$oAm3=8? zlcC=D4U4q}dNg5o94Zwx8Xio%ud5fiD|$X-tLKaH*{kvtn$L3J-(qxwZSm#$89{*@i-19NV{YEI`|u0`bn*Qj4m8MAz?YXt9$r;UIz4 z_zd|w*FE~sFKXb@3)}`en|coUWTq)1d*>f=7-_hfOX<66bj9Q>wIAj^NG>V_5tVGE z--7&Wr}!mH$_eHzcU@70-;iLO-?i)*wfZ~<`mOsphW82jRcI^Pe9pmOo9o>~t0NZ?NXVE$Qz) z4HMk$SEA4Fs#IM<)!&!-QIh6i;+iV<2I_^A+!G9_5O#&K zh?cJQ|K`urmGL5ievfDK8{Fx>w%$jtu2+u*3@`&&bUMTh5aY~|41hErIo(7(uR6|gdUcl z@DM|*^f_|f$2h)~>rIdnn`lDFe}xhDb*1K+ul*u)T>{N7(jCgo32xq0?vHWEU(?<^@f#@m zg>inzlRNi@uAtK7ZK|J5VwZjwr(OVjyQG)y8D&<87tU%e_x{&jg5R|;9xJ>{ z^BNCN+b8tNUCm;FYjQ@!bL;URht9*k1>qOiFrAP1fyScEWu5#-6cDgl(9W8>mmQX~ zNS|J^qEFXX&DtKW&qa#dNqeC5;A zx8Cy9rUhxdkkH&na&?{tnm6K;{_j>DhhWjfij-%v+nXUjo86T`iZ+FOyU94u3U9$UTo!!ZkEaXg6)V>+*KANDsBW^y z3;6OL=3QM~u}>0|T`@Qxpnr9%A#n2@H>?{*q`!c$h1r-0%H2_&m&89oGKi4+h~?^7=98HyV%7U15Hd zxx7@-Zy_7L52GK_xAXPx(C<+5?gjq>dP3S1yv9%yb#z1$@Ka8Bb3&JcepFiR166fh z@GrG*Z25ftZ8`6FZVdA0^IokJ);lDntS$wpLB2CgO4HU*(IHp|Y+1KhAolRp{kD-n zHE+fujvsL{ZOeE8%%F=(*%0tRYpMp3iC<;pbku3fY#3JxrKCsRic= z^nOB=wrCkxmeQd05A_V}?;W8O*LCJSu{iR{WP^pEztNb*@O&V&LKo&?{n*%`I$6ol z&ec1p0QhwY@bfO|oCrc!d$tYKkKDZl(ElFgNOeStHPxTM3-pDTk*#&Iaeu2BxowTg zDo6u8u%}XYEBluefSyX8j<>*n`r8oY0e>Ioz4pN~$=}?|M3R+J{#b)gzhwvmpDc)|PR8V;ye z%IdEf!`4+LB-#IeAKtHB!Kap2Lw^qN8KNC z&`F~20Kw9+Lw*An{E6pjfA9~LBgS{5;5;1B4G8ibM*X{*V37YZzgebitDB`X?B)H~ zCkXKQQa=nMo2IG^0ZJ+81}><`HVIB&Ci^NOXVjf9j5-zL2YCc`^4{v@47kHI>fg5 z((mVUQ7@k8td0N08@nuA)$YH&}#^}ee|+V3E<^9;-v>~^8YgFRz@1+(j#^Wx?7 zkL?U94K4H`gMHov{Kkm!;lr_sKn9n-_|GInowFCdHF`&O-IUqA3-x#~O!`-&sbD@L ztx=sK)2&(fN+K!ybr*)cO)qOL*VjMeSN8+uYsP1b3vzCyz4kK_&jf!pOYbAtO|nea z=l-}DA~?Sx&FYVEQY8tU-}XNj)i2kX-Z6~)E%B1DkUznZR{tPWy8!gDpJ@Mx6X0L|t~&~vqV}g3 z|FCQ2VPS30=H(E6J|7*uRf9R^7ci*24E)~+eXTH=oRf0zL6dkD!YAmC`psT=VZDSi z;X25k_NtkC`)X}6P&fIqASQc*an3ec12&$$X#Hs01NrL!b}g^ScJ%DJ0h4MKlkd$7 z3vVHxw^sViA^pC@W``brD5-z3brh@pF!ze?+ zxeVSZ0I@rU!I!7#2=DrAl}>X&)6UV`go zl3XgR1AS-rO5m@VLA?R{C7``=^+K4cv(QpFudolX*tR!fqjApzgSBWLiSB)U#qo>R z^HesgK)uDB<>UF)yCYtw6Hi&(g8w%{cL)yLCNWeh#h>!9hW-Sd5D+-ci7of`wcv%W zNT(H?2R;(uTf$1ya*z*=^YH%u2Zi1Dp07M01M#0t2Yf8*s<}>Ia&0pFJV%m~Hg})x z%A8cV`4sTs>0Migk@=GHKBdRQdEw0Gy`wPWI#u!bwG*fwt_i)4lRtij+?p)?x?Oy6 z7*mzlYH8qI*hA4;fc)WBA2w~H{iJ*GNA43@UCf}04|)qDqlV&vC)7es*ubxc{9Nkv zlIbN_eHP^7eWYA6jzDz2$8x2!vb8>9bZm9CrNw?`iBr{nu&;TnZ<65KA(CV^*vNtZ zH-e>Wle--Tr91njTjBa)-nh`5sSz4`pDjn$1$zkhvGjjdB&w0jojVVDDZ`}boJUuc zj$~VE;)fu6hX~;Qrpu7+%_;uc&w7 zZ@KZ)8mY9)1D!GmTwTD$8-b`ffh&QCSPL7gt?Ogsmhw^t~NM4R2&&}iZGgy9q1~@WG06=1WjN&G?dyR*7um&U5Z_ zXE(Z46|So)1pTU4S-F$_mD|S|I9UJp z9xqs$+}*P#YqGz%3GBb$*q6{@!Q34i?3AAx`R1JU8}8j%Iy{fAJ|k)H50G!aOIRVZ zu~dp?Fo^gjnDgcmS#osGNq+ay7+sRe`%?L?`T1G?Qhd_zNn!W+x4PE)AA zN;(MVABLFbN9hAGD>B|bDJlefdiHXm`Lr>-zi(l4NZ<3g>TC@!&`aZ!7}IX(d?Wi+qOkh87dDk%K>HWN z87v9eN`+-s_nX;ZUtB}=3Tb`ZPJ@7iM&)*xjH*qKg7tIKw53PNTEU*j4KJBGIlaaw z`bmED0sqj)S16?1A~VEVMA`taXyAl}J}uTPU%<>`WC6bg{b|SM?G^TaIHyzHxX`~C zH{Q80KY!z`t!2a)9WUVPv!B8w3Gm-WrUzu99)SH2KRPrh-Q{JA3PR}o@+x|oD#+Y6 zOVd2R0_C7;PuuUHr^BazxMc%)9O#jPKeyA`R5cfq_!IJnVR~ART^HfOfdHzzeCS>e z)+gIPQ$fY##kCQ>HsFOY@8H1yvs^Jf;Qd7}WIuf8+{YulPwIhFqE=1IJn+YC{(J-H zxjHIl<^o$U2>7-;1oKWm+^9H&H`4E^MC*5T4kYNkSqty43AAS zlqj^ei@;vzjMa**@h_R@77X|p=q+Q$UQtm|(jrNgu{b*7qt)Q4xE!`*W_^^Z7UEYJ zlC^|JERWVJ{u3Q2;Om*U>T7By4b#>e;E^7*v7>Fr-@WU5{c5|Ro@xO7a%z{ozI96N zfWC?@!o%NaH?-1R(zWD*Pu4%!{{0KYI-6X=Y)DJ91j2*%lBI0jgB0?|GOfnIpMXC5 zg_HZ38d{QDcZIe?|ABP4M`D}9xO(Q+gtCd7+kpSm;63T`?VvpI)G5^80ll&g#a8FN zoj+f2L&Z{FkOliG<`*Arb#+B}NzE?m)~-dNggJ%rD`+0+F7$tLx(NpAetu3n*`UXw zgXZgJFOQ4_Y&)gowv#xDc1Qw>Yt_FYJMslS_H4$<*(Qb>i&nbHx2L3$SS6y3O zy(bOLTk#8}%x&B(C5m#!Faf^hVLQ!~{E+_wFKH=#;?Gl_a_3>5Xvv0!nUYOP8^dxQ zI;GVCALk^ma#jbO-x_nRmo|izw4|Evfdc#`)d32MmHU+ROe30J=2$9dy4?B7n-Al@ z4zmUP2Ij3V{Tk)1E)l{jc?*6f|Fy-y_>T*f-}};>=A!HJTJklZtBK(`!N*KKt`33c zjp?60k#gce!%AyyLHv}JUeRZnF4#{A`*k3_V=!a&6EwYfz;|a9=qNxvFdu>U4R;sh z!#>W=ys@*%ijuG|tdx(Dns)5bjJ`OiwQ1EsI3IJ^VF#Oz_e<^5sbhmE{|vvc;@4Hy zt^J&FF9`iyi>sxu7v9!=Nyce|e{I7&waJ<^Pvaivn+fpwetMjng@(rY*gLO`d}4Sm zMr>XEC%?wS>0O53w+Vh94EUsmI8J@+OO4mpGnUQ)pqD+%+Y=ona(Ut++OK;x@{#Fq zz4&VLuLA#Ye}{op`9jxI3pCFR@f7q{$LXtfSy}~{qIk&<17Y_SAa?OYd0F3FW_8{# z;7fxZFh0;t4!u9Vcf$Mbs1r3~$C|HOLp&nA`7u%`_U%yTqN8Zj0pdaDfEYB7TXVWg z8~BV+f2lSf$M}Dgl(Oc$A^!z_dvGVyVtL)>_xsWSPvOo#p~dCpncO65IRZX~=Ap@K z%8$;CZ8OcQQU8pmdP;@zq4&tw_00z%Kj_D<(aLXKD}E`z_2Qh<;4joFpkyDAr% zZg`$;`WYLZ{eEZRitq`cwS=z)@V;}#3w)8D@gr{x@DU5pr`n;X+4^#;X=D=z*=KFV zVzp609$m3BEf~E&z;BIqOIW)PYLD2$_4gUO2)A4HBtCV&wlsF^!J_GZPr+?vPyj6OK zBBP#YSI=d`3BgDT4^pF1eq-3Ht?}5xChNJ&Gr;2w`!V$(U&f<C$9! z%kcAr%{Mu0;KSyX>?}h4tR{jR#7EGd^5L+a7mW_c(F+rRW>qZ89EwRJcEp z0P|BNSA|}B5AK{CFjy^!zq_e8>C~O~(sShRqWh!gaRY33cem?vzbywm2Kx6un3gc= zcWY5x*&6b1!|0*Er<-StC1TT(1oz|RH`(6a?rxP)Zi)0LG1xDSXNs5kDWmyTn3uiD z3Q(s=B;F}i3l~}U!+A6QeC_yjgXKcv_!_aM(CWuHZ^I$ziCo!6q@(rS`%sO~`P(2k^kr)49kJZ$HV>!#feqcW3%}reSEh~>Z zC$v$%iKX8XDW7{EM`bPmUUGOA>!T`-&d4gB8_<~&6H95p>7e{@HbQ;Z%h38XUA0=c zcHy9ah34NOj*yST{-0l3k6&6yOk8#i5csQ|xf*|L>)t(Lait-9*WrDr~k zrvYE5^>pW-u%B|i2Dj+47OtnS);xEFH%hwq>~95Hsemu~E3l&=RY}4)@Q@eGLp!#A zOF{j@??3wXp!|lUAAO~{``6RjQTG~Tul#83%^JohO8n%{L<{z*-ip-#zDjtr-8nl! ze{46w6~EuZSwg;481-A|a?LmbiMi%!-^SL7o1pKQxaFm{|F7ZW#7~D2UygpTQG=po z@YkjpJl3uY`5WkOIk+D!UFz_+dgQyS)@8FOqF|)SE7=Ul7eP<96<V(NN$_WVwP&XI^OGHG{8~e!Q9q6Ds6=FyS!FB!Q3v@T%$F8UVm*;K z*TC^9=}55$W&0Ea^J1h&*`EU2d%*s7SC9*SJ7sj79xoR3Yx1lJe903f99LtiF7P3C z`Mkk*b%Uf-A?TN01b-T2WxG%jKZW-X&&Oe^sp)%?avv`}$>#t2ZAEGyQVf9a z3+J=PL4&e?L&?2Uo7DvQyV)}7J)N&WU%q$XM*^CM;^j3i!I57VytdW+D)#)` z;f&VGnGx`(r~KXA(evMH5K(?B?}%Lds@$WH50#8yl{U5(qs*vin2&(plL})^>gAL@eI?&#?JU+RH8{R?ot(9hYo`AQ*>s|OAM zJ^}lNnjW^Zn^Xrm?{U%_H&sFWc!Xt5U)jK$oz(&Rg?)CpB9WlJKR;MI1J~O}SIslS zGnPGJ`T1h-|Jlarq6ytY=Qm$cs)hbm!#CKEteE{#+UTpxt=ocr-&xqt*S%Vbvu492 zdcL*6#c?K+8x4E*oP_-8@a*1!TtHZ2WQLMAvQd6S4~{;lf$ z%v>vr%X@e`o52&{-44<5dQUR<5=WE>e#El>16ZOxecN`3}Jz<^^hg?3I0UDe(?4HUITdJu8|SOv&`x- zSL2V7M^pZ;-T4(82LGv~&R8zMtHY=`rKW>g<@?snn|Ns6n}T01vlc2~QA?cAJQy!7 zH^i8rrjRMF&?LZ1qniuW)rC(vo*x|)Tu+>eTKaocZ@;q+D?`^`X*+H6Tyv2hQFm!$ zU7hs~zz1+I@m=M%k-8%vl2QMS9>-GO@3Ep*8;&-pCk3{{SU3u?7uPpURY?m}Y`x)#vnAJ#YE$j$SU!;ga{sr^mhx`bv2G!)- z-+CkWgXjnQ3kD5E(i`@i6Y~Fj_1a8|Ms#tL9qk(A)8nK(F6=|wG-@CzgHO8shVw5! zCF$7IIy{`${9faHX;uWL@aI@ZQ42axHE&LBm6kZR??CiLuQ_HE?Q>~pJo2xm4Y`hy zw=Ygv$`68v#^|CSYfDVd79&0a=BIy8R6j|#4e=+!ohJ#FTew_R_hiutEA0gm)_`Fv zU_O4GD?RC1>wn*GFIzM+n7&mnvDn8t5AbYW-45tLKz;=In$KjQ9o{NK-#|LMz5~N5 z!+l!IwWVjyyAbvb_dm**y9fF>WgB>JuF4CZU$Dc7g}veyt$kbBclErD9A&=QeI14L zk6t7J{)3GHor6Mf9^_W|Vb;2k{~j)W{^QpwhPTQfCa4FTwc=jkHyIV~r$GJ+K=vwJZ(V@$CqVYcNvAyVYTyr;c43 z2K+%zc;PlqIO02pX|&p~j=aenJ2QarPte1RBRfD5&v8nq0K9+P_yc zuQ~Fbe%*T)KR|xDY34%@IuC${))#L6#CR$s3i%+^KW~(_sOYTQH8`07gRj8H^d>MS zr}4w-?_;)t|A&3(xI(7I;qrp_MxF3^F0a~o;27!C#$W3gDuUL=C?d+-mueD6VRz~e{g!M02$VYQw)+v5U zv2?aOoToX6KYB*xpQ*G5`X>=Thkl5ww?Z* z4to3gO0M)*M*M2PvkvCP%evIjvzj)vAbtY|{V?+J!Nj3booSR0g}LMc8g6^PB})A= ziud!^_i3=)muuAvNffmI1@=%ytq-q|&hdIbqmK3yKJx2Ng{qn8$G8wPt_N^=mI3t($<nHO+~-(2(lYS`3mvgm7#9ihn0R(7dNBlZ5)edQV9Rlls`BmQh&EhFpt4h zS+Kqwp!v2B_LF`oZK9xk5tIG|H#l$2uy4jx{pQB1`HdS6SlfZWc18P_9aecw4_Eie z5+NSF-Hzg$Mh|B*{9NqQefAY+cDC+uFKZ8~_fUU9?zp7f3Fori=y`*`&pDzqtdAG7 z9)^1E2MMf!ptJF9g5SymG3>kQZn|qFVZSn8oyoC;{>3=Vq6|%^}xOb5%XcA6QW;&McK+h2i>~vq>h1}|EqAS zgh^qGU>~G*jz~-PWe()aN4c}9#YM$>YZytHJ!eDbl8XvTV!@wqA_@!r-Tv**HcawK zVa_4nVICChA?%Je;e+%>qep|)S=sa+Y?ywNwya2QTiP7i7VHuBJCmQ$4PNdZUL5ud zU7Gtc-5QsQ_>!N?eE-n}{b8c-B&Qb}KU{IF?j_tWn;vRI)h=$-q-u`VD7nSl$})rf zX0uDepBcy-fj$u6L9nP}MdQtfJKtVF@ne_9hOLTQ5AFS#TogMIZv^`_mIy`VZ76&? z5I@{Mvzm_YQ*mCpNb^l=vq0&hGi6@Oh{oaBOm9@Mf3$HS=&@#*Le= zG}~2tyy<7U=Gt?iM3b}U?*>lse;Y4l`UXLK8>XxK+jWuCRw(21hqG&t{^a8GG+Gn24wS!Jak z;tPh8-<0VWODqowu{e+Td&J6b6beO(IVK~ci1^ZBK8*5W{f~Q7Hpr{b&4FG-(HfXX z0ez_MMJm!K#nKLGxTP=lY~nxl2K~4lLv0FMKJx>KLIocZCZ<7Ow}d&QE#x3NPambh z`|L9grmcVQVCaUB#i4g7U%`U(13J524vz%$Y6SDdFpt}FOl{rN8rPz^xm%3KzwYh@ zyfXZn(zMRpfg5FK?DCqqRH;JgozmNhc@!>V}fq^l_KSzupHt_G`UNJMOu*bDVe+Sih^FOlLy zI$_@u^n7-iw^raJ)_)|uH;4Ki@akxCws!51tkAIMP0$-Pbz{9je9_u`|5`b?A2#Oc z5HLM`RWnw3qz2(zr2ATrXF0=%t<-`~0e&sjhf)5rowl>Po#x_>_=bj#WwyRAeeV%t z720>gJ`1u=*f~M`wc%Za{1D=Ym6yim*1F(DU2k2a?|n^sOI!KzN-5QgYSxnA57Ih0 zM1BYDHtoV--};n>Cpu`~0WV@jCy8JU=es6I6_0E&{pBZy~c{QIW$Sp&VZ)b0%1i}5z%)5Bo*j02cRgw8P2z)P2cx5w1 z{Yre=0qF0e=UWv-&QVS`FI&3g?*YW84R^Ut#;{aTMTIa7@x!WMojUVanyUEfxsNx81pX89n_z7X)4hD3yz^aX;WMo9+yth*-%0CSA zxLOi2UUym_)(|Idn}Qw>6VgxYo8{v&$R}XGNuD;yU9T$n8Q)R>^>RNEKRG$+Z)|Xs zyvlPT5Y7J@%dS|Ov^-@d)XHomxg$@ru!e*ECU%c(JS=#>J}QoM`?T(pFM1+S{f2qw zWt#=vHY*zsFc;vf_bpjHnt2}vRT#@aPXqKpoG&80D&#?kIPxdF^5D%Hgzt>8)jLn3 zc*ZY$Y4`#_>z_lo|3MtVsddXDW&zle(izi z@+oFAIuBJI!Ji&Uey{2i&pd(fE900vvV`CHnw}rGE(z?-JeNJ-xL?{kr(PZ5-@FQG z3CqgP7@LfX-c22sOisTmNS)#@l6-S4m&WLUo}S%oJj!<&b~}D1S(g3y&p$C1xNg#G zy|`{Z^2Z|S06@m?;<2lskv6G(*P^_$?7;Cgbcs!=hotm&Fl-{mn4w=ak zEj{es?t%M%p|fH!Xd>AL^jY2IR*1A`gcNt<(etbcR+8LNc}=@sTuvS3WAwAdWSVyI z!-ecb=y$_Be|m9LH|vd2ONW@h;AbB`U7GwfJ;Z-mHSn2Izqa|Fx{h=*3)wdE8d@#U*IoE5t%r$liUUnu-{I5aQWkH~a6aA-}9T-4y}n59YU-zY zeIS2@eK0*&B9Haor7Aq+2;yOZjoa}**$L^p_Uk;C^009O{c&h-*W`5nt%vXslCwB& zhhyT#!ah;RZ-HN4Azj`b;g&VI=Hb(JD!ebxXj>w`si3qq6feLRy8gu%9#5A8Uc0@S zo`%CS7;7pM&iMkKz?|X5Is5xoSUoxL$46JVUySE1E`ie@0x?EC@E>hl!yC820i`Q1 z8w>{jh5XI3E0mhf=mN?h9qEUX?n^Cl2-Ho(lPfXcSHZlB6xpl;|G6gEB4F#D#67Go z`SP*v{3^RWvT{)Ga}A&OP$-cx()FfkCg^%eVVjk%HLvbE*Bg8aeSfXb^+ls<5Es-$ z`*Z=nZ$x-N$Dz);aV*K6!TqjHW_3=S;GkW@-_nD+psT(IqiDqTD-zY2JDZyxI_IP)DI!u(&)O3@?&~_ zu{q0KbshI(mcPJX$0ans@99~}QF`MI{;G|BU%13gfFCpHw_hH|ii$^v`n%LO(>^uYeG`bp7kUsa} zQ5TsQsSL{uO=mP8M|?-XZAAPfdI6)u_VM|LET<9{^cxNDYp?mj!LSHB7h(wLi@Yt? zM)N@kAM_8jP6PjL9%e`AJ@Q2O*;yXa=Ls(aHlN;IM!0Xy?Igfo|I`1|^|+ZS*w1C? z?d(XEFydH!wN^*`p_*vh5x-iTTF|SapGY5r=(D-2oK~+%uVDJcuO3}t@@w9` zT=OuuhUkm_Uo`~rC|^38+-zAggWvQM@U7~cVpN|_UoN!ma+~Ntde%#_19~&{mYFmT z_bDcpd0te9edV(eOR||45nd5?&l#&M^7&iTtrWhG(u6T8TwMtAby~`AA-@9tO&aWP zf6=aQDgRm$;v0kHBBi$@@rV)oRZ)U2)YrUwV@yg^alH&T;3)k4952`WVC5d66Os8x zc&L8i7tTi*87UstE()&{Tko-l(9sL$ozFYnzf%{k_h42@|3m1%gg4uAz#sR{RK-cH zFeh$O^RCxU_xjVX&C5PNiZdHhYgzt0A3e_+KtuGk``QM-(jeZHFks%p!jjsh@W5bJcdafAL`7wpSPxI^8~c+qpb z<^$rN>@{e%U@Dmu97s-WLUs~?PoRGY z{gD2mp-~MIdcHNl5ATW`(pNF1VcCvQue?2b#T-we@F>=Ibm8|zLNpol{R_@Ha6Mz@ z$dJ!ffga90k>TYtRnQbi%PTH~`g1k{KOLmicL4UO+vTPp{p|hzJHQ{J{TanY+Nnv6 z=duP8;rh9}ScumS@zS-sdoAI4jF95+7EEfLWQNzhvzzx_K*_m)`h7L#V`g@&2a21W$ACIN4s)n zi7)y4kDP@&W3C$rh*=fufY_NDz6`0{S$- zmoZki)aVS5N-7}jum!xE%gZmWXL$3@4&X%<5q}(uaify6;}ZMtuH=9};_?rneP=?_ z!a{lAUqNp?LsBUBCd^NeAf;YZce>@&{3|Jonn z@ik;OR?(|@dx=fD$va1q36G0h9(>>tBfjun9)x&0fIXqvGSxd&J?_l-q5PBh7U(zn zrb>S6`56=E5dXblT2JKLcS*;zb4|=pe3r&tfnMqq$=JO!qbhctbC>}gdX^aWsUG2z zyLNB3tb8#|a|F`UUty z5d-br`Y4~Rif)vA*4=3IC__)Y{>)GD8Mt4m-=bNkv!>5^&Bc+~e-++D{%&mAsT|%v z@U32Tbl6-^#VhmZ2#=w=_~8+PBxOE?|AT2N2K`a3_%$(6y$ zoR~uRc$Evap64iEeH$qu+zrWje9!L%9p)G21{P26oJ)F6UdixXz~+r6i6ncJf5JX% zM&R_X-Gifhmz6^OUVBeVv!^F1l|6G5`YpZRcI9^JrLC8EK5@1`Mc|)czoDI5$J&ln zSscix+l=#yySv?-5+`NomPzH7|J9o-Qsbxi$tWM7@0KdkA3pL#`B?i(F*oX0{ni2 z&bWSZ@_nZDh3{rPkpGOpeEs7sOYnLDLEb#@{|w`N{U1}Zjb`u8g@E3p1;;S9J(zM*cUxEL;=V!CsKuyic)fz+g zitsXWdJj!eftQ8iaahITmMb_mek#{N8u|PAs!4Z-$ncDKeyI;SZ*(&in19`sqQm(t z4SWDrzLt|fKmL{7!_2;dIGa0~Yc>GiFub{=Qa?uiWZlSx>!*K=h8lNNUvNt*t)2Nd zAK-eMizF$RqIspmxU1?Nve|xv!(e~U7?`;?=w+_jRhshdBzk_eH$=+q4dlyI?o2`a zf_|_I!56=MWpRwsoAYSC$~f8xi6f5Xb6>uMWK#bga8W^cLQxsxW? zj}YMcK@aWg+UT4EdeTdT2K7&uqka@It(fI5b!|^}*8t56^eg&x*QI`de2CuCw*ve- z@XO8Z2z;(_I5;%q?}+ybEA(1$Pi^cDaRv1T>_d&xzo5fi$B^#R3o>=Kl?D8mUlY|d zEW&PzGySlw+3v$MKb(na2wB5CE8S@sxm=$yQx>bLo)o|BKfTeRowT35t3|#vx_wsl zi5QzVvpg3*jGh<0iO}I(lWKye9fkUTdozafJH3Ako)*8} zZ>n!Dxft}bn20ZXrGVqMcGJN1tgzTH@Gtx&ky69f3GlpJ63Cqa5)&C;58Xof%)Hm6 zJ5>Z{BXYDJ_Q}(=V#g_YlcTK_brrW?CfQle{` zZq6MGwO_NM zJOSip2jj_Iha~LF*(Rq2dO4V93-|Pe6W%8x#cl>vZ~cxzmjKhCFt?c$S}rVr-kyfF zQZEbRQ{jI4Noy^q*&k(P4z}aw!CnUFl{v8QPdG_a*&FgLz+XIn@k$Ef`igpeS|Xb- zwldxhp#Mh?48N8Q{;wDKEQ*^chj%W(k}2@V5O1}+j}vZZHbv!`!TYWv-Epw9>AG|1 z+}mJJ#d^<`V9+oXxs@plD5}|V-*?$1!82o^$8p-D(?T^*7#mV-SDu>a~me8pbr(E>HTSl zKNB8Ytd3Ww9*q+dQH1MZ0H3aAZnX8x5yx+yVvL-!yrM!1ny+aM+jjq^SVXI8!ui`fyxI^6}^$j5mvtU5xE z3;KH=J$PoSr&@*hN|=A8uliQv2zsSCk)B_^d;vbtFjkcisr2U2%KK|Q@KFCjzlz(n zS-54*Q>TXyA-~1DcFLprq4v_CzQCS%F;cc9%9N5${shF|UcggTwT6wZ9Wd42)g$7S zPdKA`Dxz)rksE*D6Z}5E&)C4+!eYd9AI;B~2Y7!kDJ;Rhm*aZFc5WZw7toJO&k?>V za$F}?To#@W)YC!lY;AlSoZ=HbEq1O*DJuZESLBZhM=$LG`+@zBN}ZSO7}8?{)wh8! z(}p#%rY)AZ)ZCE@PXhfq7izH==;_afUnyvwf&TR{cITCijj*a=8r5B*3HB|x*vfhE zdGpoMio6nZK6qX>bXoI(w-@)XjTG2>Y*>S&RJJ-lr4A4AZZ`bwI|~L*Ld3*`JOlcS z{b(Qa-!4Ha`M2EX~ zFi8`dC`J3~9{+wObVSID^C5_>mxu>$v_14=s&Y6Kz%OCG+C^B7@aK6A7GneOQJ`O# zhf^{2!do=rZym_LM(8_w?Cd(0 zm0S9aa^dsb@Eb~&bkU>jLc$qYcXwW?zx&^Q0kc`%ZIB;-oxdn5BC)$M6d!C0``oc# zY^-g=JG=(1^J~$)opW(C@`bICe@!puTKI<4_vySr`e-G;f0ww3SckOVhWH;g@5mxM z`3p;C3qiiw#5d>jM*kr$54o$5TA=4aI}@<82=J^r|H&LyfxSoN3H$3Bpl4N=?=7+u z-Z$v+i2%X<&vhM6QVW9qOrGtC#SY&)_NMV)0!{_{f9Z+k4`I_;y#Bw0k}f*`cb+r1 znZ}w;^=-Xs`XI2y>M)wY)WM7k!;e1?Sl1K`!TtIeNuGF@R`pC!ac!j!F@g_`BC_A-CGr*8RS zAZ_W2sV>9^Fszm;J*L&Ckf!QdCcq=e33VmK=DG_sE%iBNV%Zt{JRh`=4(~`2=-u#k z7K&)rs_qJqkz_;t$El$elM4zRvn?@0KhPIC?A*Bn#q-*!+Hy^VuNxjBD^c9tv*IPw z;QnDAF|WBv+YR!C`sI(ePg zec-!23(GJd86}nD|HJcI;_iiq*P!_vy2Gz4!mN^Z89yf1O#W<-QirO_)FjDhbqNRj z7vKv)&E+%|>eH1K_6dDW$<;a9>VQ{FUsk6OB~kv%3!{4RLGSnW6B%`|pIO5+bB*{k zIpxU<^N>G7e_pPWs`gm2(`qPh9?g%2d(g<)8frB|7kk0~zzZ1J8)x;|L zT#_J6pMV6Ca7OKT-T15#|H4xr@DHHpr7A6@wX(lwfs0k5`~xc}T*R^mME%V+%n1Bp z@cCAy$8Xf$H*3E{`AE3SBbLQ}`Q2Y{zUzkPIZju#g`@=kB*E1J@)w8~A>?j7j?Ke6 zYc)6~Yz+leA1^;4G_TvXy-m>UXBCzrKWNy6imo4X z=;Ux%_ZpwB8wo-9JmkaWzs6&;O>F6Y;Q#!;_s;gy0+^51J^A8cl6%CNH;Q3NqAAm9 zzaO;8If|Zlkq-vBzW@<6E8v?wgZw|;*57yk>SVuF2Al$I!eMqN+TT4}d-f^ync#Wf z3(}I35=)#@#!|BN{=pJq-!xQ=csq|D;mF^?epte-Y-hFK1MXz&csOr(E!!E}B{Qo5 zBc3aW!)KJNA%Y$T&%uAvSg+@FZ zM(^*e{`MqYjGG1Vgh9tukRLaEmy{apf&53U$@ow+=$S1mqrp5#A3Mr4L&C8e=8YGl zxh!d@FW6Y6Rbk;~VMTG;8Q1pa#c`rq)cmJU@;xWyf<46!cbJ#4yTS@nas zn8q}%c^cBEFs`zck~@Cj+lh;f;b4#bdv8)pN{nCACsKDOp#Er8T!J}^X1``)vcC-8 z@6DLm{SKf=$*e3df7EUX{`XK64s+kK{QgJv0px!T2NNVyj!@+*nu>mI?Ypm+qJ3Au zPal3>3iuxIMMI?R$#Sc*T5ijkq4-c6o%bYlpwq!G-(FG|{7+3|PcxqK>#yXLHC}KY z!JoXN*^YRZR5^I30Dq+n^lZ!_KZ$r`N=&;8{BPIJAi$%s+VEj2_-7s1f68sPz#q0! zRhgxs@1wt#mMEZ%n;cS|IR*Voi#)B^F6ie(NP-Hv!2W}aZT(-y-Y**B!u?xtiTAab zc$+`VkGNKb03I<+iqoQKO0TLZdiVPsI&ZbXIQ-=MGb~YeJu?(vVE-~>bWB9%T2YA< zI{)E!9L63Oe!OwdwP~}Ltlu8%pr+H{f2#AX<)opW`dNwPE8s{RjPv#D2*g{!cSUe( zakjCn0V4+ZFD}s&w`GeOLC4!xSExN0>^pkOL~Dn_!wE)F3)n|9{h&E>^qD8K?91TF z&>emRcfy}L;Qv=ev)x&!9~T~ey{9)QORhCi>VYocCD;!0E(;Awy+eA>jM4p)407}= zIhE=)j}jN#JD|VD$h8gEkQ7g@>P~_BP?wuO{D!XrGPnSIF~El#yJYn%_pht_a=re(-Z#VtS9u%M(o8B6f%JGJEhgZXV0+!v%yoCHY-@Br7-{S<6>jtgo zo0j?kpGTm_4Rrnr3BY?`KOD{e_@09v2lHok2b;aAj>8B)B6$`c`QY>+ z=C)*rH}cQM2E|dZ+GA_W%Z+0(kPkokZ=UT@a$6AK;hdT{zb^gqUkYb!et3V= zRNtUe!M(p6S1-(8#$tZfSPSMq9NwAPFb_w2ef~BTs^_-H#n#~kI==}GBY=k)&cptY zX?}O-nuM7!Vd$@Mh;Mx~3dtMOTVBThtlwyX@@0~LTW$z}U`v4aV+DF93gm4Zs+0L~ zXUMnNq{Ce1&6`&B>Je|N#nw|m&-mYc<0An9mr*~7q-Ld2uJ65Rz*~w7{Z)))0Gqb| zN7T2+Gu^-cqg0Naq;pA#t>KolkWP%qX+%lgrBD>2&`c^hR}!Iv^I3+oquqRrK zkM{Ceb4L%N`rv#{A)b(V?8*3%&2aw=x+!iBc6RKRyqKFa5xSu7PgO(sC**@frww;( z|HCIXRK2fX8U*JR@!{M1?UR|~+bd!H8+gp@jEtCv%hctTx5^`X#d1`_X_tOmYUH$9 zC1Nn@aL)hfmE&Jb29G6h5Z`8Ux3t5$=1-M=;6Hj;;ah_KG9oM|v;!|7Jky)wxzH&A)$b^M?{G5}885ST4nx1w%vHQag9nOdX7#0TE&ZWl z&|mq&8ugEO5TG17;_4fpC4~5_Owdap5#%Jsgw(jrRM4wFBfXP!)Soc-$(1LVm!KU^ z8y;-d?Dx9hi{`yhbd>g<=iV(*xlrbT{8b>#+mxWaT>ne#;fV)|Zo1BPYrS~Tj~^59 zlLgPygJsvZ+8aJtZ$y1&h2q;lE!%k)LXw7P=-@!aX8)G&?VL;epR-KqZ$%EkM{J@5 zr*lbVAKOox@*tmj)k^(L!`N4k9Q0hdrTd!`+@$~AQg?U(`#<_)NCJmxy zk+^JE^t{ZsZ)uv6FW238@%%U9=TP)4i8hl?#fujX`AhcCax9Po2PnFZmwyel|sA*{b5$S6AS-n*&Wl35P4Ghw0CVh z^ylyTcROQI4T@)j!|OeE5j!!k zkYHX!GJV;LhQ!)yTPs5Z&wsGK*xvs$eJ*>n-x~7g>VqFLGM}B%d;F(96zYpZEXIfE z0*6t##O}wxlx4^V?Ajbryukd@TB8km{f(LQb!BK?&lihbh7h0rm^GIY5y>+UyL==m z8TsQ%^Vp1$Qx@Wb@gpgN6=C~Q#I&>eAU>&Ai=qnFbB9QSaH-~f=?poDpP_y>M7M4A zjoi#TuNDb>lTlVahqgGTnOYDxXO8?K(@as=`tSPNp@<~O~t&W zOEaFmfv;C|&l|K_tv8u1q}C98?{j}g1Q&vjxVC(eeSJR1P~%%sN@wzf?3 z8t7CFc!}poi)?+*e%`ojG0DW){@p96pVSNn{RKRufm&z=N1fNW?{_hBZ219vJ?NJk*Ov~x zc5k&lu@7uGMhR1CyHt$)JyS80ruFWv=#JXcwedwuP=2rYKJ9PD-*T4;S4D3`_jNu( ztL&r0riMnB_vz?$u$yOtma&7ucL`loKWw7p#!o!iO(!1o4PcApM?CHb$y{B=uSal z*Z=roTk?~C9a!Bg$WIH+8-#!Am0njpf$$!~K)Q(B_;NYLXr4E!fAAx;>O-{JRfp^f zPNdaxFs#YKmR_(^HEV%n5B_d|;!bCX<049}{Oye4`*s<-8*Y-74rAP%65zpXR*v+u zc1HhG&PLSl*U2@=DhyTVJVsAe@>_0=;yrg8qK%J)E%q=T@_o?r-H~Iram|HMkJfMn zYsh8{v;r(>(aF`bn_Nmze$5K>v0R??SgNbRDqQ~flK4|*RP6R-iSS14W?fxP(mtoQ z?_e)6=8mB%ZMYwUEbS=V-+@J28@mt2qW&b7Gd(OVf4^)$%fcJdoI-Zp?cNq4~QT!iphb*Ipj~sPIO$} za7$$6HUjfOPf%&+|IJss`YkaF?WeJ6{WH8oOOFHVEaKA-P^t)7(k_a9cmJGTo<{hB z8DsC9tU!_5E^lEa%md(IO|&2b@tE+BRp%go-^Vk&ywF%CRQtH#uCfopduMMtjGx0# z-te#8G?s8#J;nK85pez3@I#4JPEqRBNLIeD+ zi(eL}8nW;xb$rFfSg1b$U)2)?HdTL|es|_PoL}%?)DpSOZA9DBomg$Z#x#nT7bty- z1nV8y$o?{&zJopK*IfPAqZRNN9y2_kh27M?*dwxI5aDltPo$9?yKfVO5>FRnyXsgE z=r^w`nkw|<{B8@qmbpqF=0BtLYbB2OqIlMTlDo>FN+eLs;3{g&%DmaH5q zh?t3JOfT%(1pH#MPz#x2CX-#QaC^PP@ceYO<%Ell$iJp1|DC>z4a~=Fu2q*Du>LLZ zr)(P=`eE;n%M?QEqIXpZVn7&Hm08apyGI0m;JKr_VP4GDE1llp-=Uu9b?Q}Si}u}3 z?B97KDU6JwVLZ}9nI&^&(eoKrfgEmu`JMhhO21)Vdx~Ivo8D2TopVL^{P-UpVz_A6 ziAB5|4Y6NUviy zE5KrxRL{C!CQ$_U%CN#mJ1egu{*+x?buPlgSY3_QmuWGrVmV`C$e%Iab?9%{RQGsl zedKxN!6=h9)vi)5#CJU!vL&cqOZFs~)ss49g*NNlop-JHK5^6RpAj8Yh{v8#v;{UjIG{IU;m#cT(|s z6z>{tf2%7UAw%YzY)RY%^TU`s%isTt^5@_xuV>=a_N9I!U36sMK|i@+5q@xOH?0Gg z_8RP4?xM%4I8}`$_Z{eI4j-}6cNQVBz>r>P{b>f-e~jed=;E|F^6sL*NaznA4Gg*# zny!4jnBKdU3H?PdALP$?Tjx7n-}6i+%Eu_CBCokFd$oDL1{~K7#vjgC)6;~0NpdHr z!3Wj<0)w?a+S5){N99~HZ$1pq$DXy52(xV#b^W9W_jQnxRH)TaaJ=$&cT5b#SCFrg zXg6dJ5vD%;x`6PEN^`}`SEt<8P1rF49f#u&A^XHXh%+a-Jkfu% zy5=?XHxu*dDlpG#E<_CFU+DW@mWKI9`L}r|KytF_`gpa|Pl3M`!PO-p{EU1{RV42i zyTV89GE-OUHuK-sju_tb-*!Q2Th?_VPH+hFvYNiX+1BWmVQXBEr^W>CB zmG|hrQB0)^)9ViD*yesQ5#)=WmL3i2uXk58q{8>z)kO}7qZ!rw_@`%kBiK*i^W86` zW)!Z9^pTi$vxf7Fby<>3Ek`PeYZjyYeHt@0+Q%OHE)_2$Cxqh7$^h?7DbCABgU$?5 zgwM@FzdO63U%_&P=HOTx*#9SXGSVV9qJu)F{qvMuNUnCWT-4w6;d5Da*N>~UJu%bg zU|Xusl#dt-@EnKX7ZTBCTfYe>3JKh#aJ^iD}uA-n^MM7PZq1fP8)>Z!}Yn=O`r^ z-@+M&@6+ODoj8T!^*?v+_^X3|t2XBkjTF)G)nNwB2rptO#T9<({Ppn9iOmc_JT%zY ze#}8e^TaFVF?3$%t8Q+8)wM~bta-UN+{eM-8%so_rJGakyygL3!J1(W5={7niU!7^ zp9EQI25)Q z^k*$Y^O5{>9p7hA#+*?;K?x5lv7KCLINl%9BskxdjV%!c_acMzOnK-&8;18&U#2N6 z4LaKJ3)usV+E~CaGCx}2J=XJV+6f!z0@yYnGkPw_- z(6dznz5laXTfY9IgZ!Uy2lOuzR_?_yW*t#Ih(cwz(F^7EVqRZ~LHVBE8F`J;<8Rho zytei#I&brCo|$;kfZ_Mm+MEEg$a`?m&=x?gYP!GHT_zC~IYSnVoA2J^$ z6@%+LhR-(}7(jRv%UM`iIsEvE!!oSCW(_EX6UU!r+)(g0f40~bzRw`bG_KX0ZDY}) zI^R1enOc=omI(G_8*|wsqsI3Q)*hlYf3%t!QAPRFD=>U z7T`P+PK~$^DqJ^pk%4#^`giO53Pg0}(y2SWep-Nktr0$cydT*=?A8)-s`ju{f7)`6 z0MBrko0k@E{l?6><1WDObM|I#i+*ifut@(JikGYNhk6U-R+L-iUQ!kO{1#PNqFhF; zbZ`FsJ3cM{$7gm}*FG_S5B%vUrlj#K&tZJ1r`n5&)=w|T^OXd?UGLXwZDDJ8ZSbye zr>ftRw@(XSEv*Co|Is!s=u>0WBS#%~%}7SG)6~{`!TjHlG>w7oF?)n>oeOl9iQD-g zJ^voyyO6IHO5pA8)%vEY!Fe?PSsgWdaz=RX=B}57zi7RJ9qKdXqa5_^T-X0(O@#gR zwA`oV(hL0KR^T_nyz-9zge*C!+m$NsEbmzSmmd^5u4PR8E2yU^;`;hSKC3ZKH}C+T z;RWZ`o2aC_H9hTB5(E3mX5raH!al!9#wS{Ju>da&u<#2$zT$udTStJ0!ErlIwz%E0 z!T<_&;E%d*#?{zg2=x(O{|&{9tZWB9>Doc#U+nNQ0e<&ngd;ArSiFzi3-=B901R1M z-$|3|#?%h0I~xD#f3z+Q(kQno2L3x`N8zN4=(QzfhFsyacm2>W{ngE(^*sHIrx2&M zzeEQ`n>cHEf6`nZS=Zjn$k7h;IiTLy+UBRxio7-y$)m9%`)TA{ir;1kS2k@@$i#a!eVqj zDjie{94_Cl70ZSGBIFOW7tFTPcoN-TodFMc$E!~I0QhObGCu? z=&US%6=-$p6mheCe&!v2|BD zE1$HC9I+DccZV8V+jclD=a^O33GQcPS!uie9$W8h1+HK}ibmSRg(TPYhd3oJ1N^Cg z<__^O%ww9Gx(fR?NVo1T;kTyuC-2);QrPV>5n0$vbxi<^e#zJcr!|@5i0>Z1l0C?Z1_*?|2#6Z@sdEMWq^#ziiX`27DC*;A;+F zBoc|b=0Cm!TVDix=mA|RXUTANY?h3uAYOFy#?4@N3%Xt|+8T%c^* zE8xQhrneUE-20$^G)pXEHU;LPsYoEcyUDQUT0#6DNEg46Si5CxarY{L{|;=?D43`$ zJN)CcH{55?hdN#&wB41m*Fwkxy*BPS@ap|>r4dH1J;Wmx)781EG-Vq5@q1#VCYslT z9n2{C%e}pIc0H>lZ9km<>5-ER4J~QMJt zL0wpIzj8m&XxHtV11=A#!fTiZOf}Dr+XUtLn}Baay(~pkGFGuc{CB^;C5l&>{)L4J zwqXaaU40#Z`X`uygiH^c<+=qa-F|}lON5F=BgyhoQd`y@byVMQT%v*FE_(7ly5<+e zLtKNSFJ8I(oGs(7i}ZqcuZe7)FP~(8LNCUx!wBUshK?Vo8rmOaBOV4Y(f*Tjt!Om) zzWhfgSFeWiJk7sZ-$>$YX^7d(!`5wbQ*LXQP2htbQ?4K25rDrMU#3#)Kc;W)p*GHq zSrr1mdXd~vTmM^bsp+<{f|^}>%-^wA;`n!5+$^n0P)&u?K z9u|uR-g?zOcwaG}5C$`4{pnkauio->~0{Tt1V3#blt^skCpN&zH7c+cQQ0K1b0a zp)~FQBlausXTU!ON0K5wmMvR!(^fSP*%xNe?;;7QBHrqaim_<@4YYZ(@^-5gys|GH zFzbSOlC&$(pEG^ctnIHW)c05XeAoMS&6w??p#pE3?7)oEz|6^o(a8&2^xv7w3*rwQ zahT8Sv~fH|Ndei9;KZmr{Hq+Ap~^p|wJEzUqkJL_$KApA=R1Nw8pZsFN7`Dqusdqj z?y&}ZVgB{Rv$E1Z1~+U=y}(|B-hv3#{;0(5r|V|VBmANA?MoW<|pjdH<>lRD0 zyjbIi4?^*5q1LtRtEdSW>Ol7&bMPG>pJr=EG7P8FDp39MRiGzrRlKYxbp-N{ZVcv? zD?Odq)b_16L8K(^yEYHhgJgQZux#==ZDN9#;| z2aG*zs#6>MSxY`HvhDZ$-w?^+1+`-6C!nnF>*;BJ`S@=))awENrtFYjV_$Ex^p}wM zCxN{S58KyPQn+v54xSm@Ux=>`5IBAB3@Y{!AYWvGf9RmbbR2oaaH3B^{MwB;rrJeEZ~X zNmTE^E`i<+ZMpWo?f--TUIY1z7X6&ffR3GJ#cKh-@2q@YtKT#&r2AGSeBKa?C>lo< z-v9WUjjkf9pRvlRA_R&zb1r!~N!*j?^-e?J z>(Kf$Jx{*Mq-}{;X^nrZeWLU3m$M~U(|p5ZZs_J-l&>1(IJijLE?QDN)Q#$ET!S-K zL_9tFLegp#$p2};zwtO_qLQ3dM(6XoW-=Igg}N~U{z#t1Ik2C+O3%L18zNVjx7CIc z(RxwxRO)RT+}0%DiM|8*`Jaesi7M50dyG@po(2BC2lL;1z;x{~8gFn1+*g zzpb4+8`+SP1 zcxi`{iprjY^Ju-9i4GW7C_zreu3#{4Kg^p&`8x0mo=tTMS=+;Xx)fj`5&b8&{oWxe zJnx|484;l?p+9xw|Bao!bLqc&(O#MJU1k&7S&qm5WsqvH(PA=Yb!}C%2g0v%+FP%z z-*{JQ+QaCHZur*Z9H>_b3GJSR`PKK}{mZ1I%zKr(tBl$p9l`jDC?u z_Q8;v-a2O=D^WJ~dikEy?Ddla7zMOag6c_^MM7`iTUp~Vg4JLcg_c*(0zOpLx8ln8 zC?DO%ca>_M0DqCEYF~~sk5^EbeS-2qe$>sK92dPEjIS0vYs5!Ql&9JAx9ZIaWhq$0 zd1N_r+F}%*h-CFPCJOxbnVe$l7pB$C{q=3gpOLk_qZ5@HRhw5bz0=A;FBU6ONh~?i zV)`i|3BK=ibvEezP)Gkc{#CCA#a{-wN}7pb((k{0w7Q*FeDT2Af~4I)!2Zr*+NuJ5 zlH3pWJ(q-wi3+CR4}cH$?n6CwOs96c=F4qeoBV6Ow49Azz@}>^uW}$B)aju_jh`@~ zWmnJkE6E`|&A?n({V$2V{;r$-whGXHJKnzLHQK+hi2*X=U(Mo34U3|e1l&7a2Kff) zi@P~Y+N$p@_0r^C0Dn2e%BE7yC&i>%)~ijx{fGH`2Q1p!0&gV+>0LJ4eEXj*@7Kl- zb1wwF?AUq^&U3wSsy9%bAO5E}MD$s=yoOVPn$^Y_kz( zm|x~JIf`LR*r%SnI)7(LS6xs|hW8*1o@ZfbzCjE5+xaR>d}zOA#^QK|p>+pb*k{@l zC3rt)7mgBWp6L8nmgZ!|JwD!{;99!`^zC2d6eUByFZZfkrad`A%ZqceJ<%SXmudC6 znkKL(QP&O?X*M5s8M(_n=GB>JtmT{SpsNyjN81F(5c^!$%ie=z?B= zds_|-)t^Z^jk;j}yNu1MOP}w~qR-KuJQeVjF3+`GTeI)AsJ6}mtlvauXKDc7w%+W^ z=av=}FB;@@)Z;Qo{|Wh8DGT?NXOI|R>ptGT#E12+5%I@Zxfy9Y=T||0zi}Xn*T}?K z{Vl5QYwdETRIaSJS99jBN;J&(t`YtexEAq&^*p4a8cvBWX{7&71im1f^~Tn`q2W~x z`&?%L;sY4W{hn=8ir8;n7LP~vj~uQ#=^FEKg`ydaEb#yNFD5%mg&wDgi3p+3$8y3l z3PR4(=klJE-I4llUgwrd`jmz6G8uAuCGW&j@}Z|s8MlNJ|2d_nA>^qenIm=Njq%>wy*y#UYa_N4mfpEo|wLirZO zRCed_zNcGT*ArlUb$}0|lu_#N{qg-Hw_?Dbu=&ojz=y<^CAH{PK)ej^2mJ*NQhRN8 z2KPqHWW1oNvVmX3z51e!RC)uI1@S}<4eE*HgL#zAzE zh(V%tM@_FU@Ry+%{#euAynMWhs{YCeF8B~(@59u zysG&3={wD!pL9OBQ+m~&uDZ~wEXiEZw}Sc>cJHAef4j+JFS_2d_WfRcVQZLY9vG=2 zsiZpC5Ze<@1U^b*Ug03zhncI3h}_d-_Ypqbwp&_c$<|7Zp?LRi5wFimEwe}S+{|~s z8%Ovv-^s@gk5_uP%didbzg?hjRj*=U@l)ORJb7Nw&qU2WP^R7=RoSw>Y^E#hz@x(C zYSga|L)Mm~{CO4^8-^nfXe$aWo-4+Bzwwv-JA+;8OI-YIij~&Zj*YUI<_9daisJ3P zS%UL);14MlC&SF^y9D>Ci94HJh-;8Fmlq}0@sa(XcWP`^#nsB%mRtBDeAT#;U{9fM zdem|R`bF*Fyn1R~;@Fuy-XF07{BaZTxjSsNjYJX)T)WFrzHTh0+A7hJ7tL{c#0CEU zE?%x*7zm_``?{ZZ5!jde<|CxFT|0_HqkNRtc$J)J*WQWrSZ>zQ|EZWsEXnYqdj0{v zPP($4Kz~(Fg+69ac(a@Os8|f~0gc1u>lw})#e92s5oUw&P1%IxO@Q~2L&MdSmVQR@xPcPS9Iq9TaLAl@ z5AAPYfE6D{?N|3mtwHfB)a&`>RPl~wt(lf{V~2sC%UUWVlxC8kkg=Z!_Jv(-0sVsb z`-It?3ww0!EkQpN`q6=(zGFmR8}j2WRxUN4JtDNnSD`yZu-^7cJGA)rFBL%T2=z;` zOjRYdNiRxczW?xEwm8{Np{PP;f{z(}vM#N|ZKEXVo-!^n=ZM?=$RKJ9I zZ5-8|AM4J1QX;>UsQY`vAf;8MfRBY3t@})p0{kBM@aE6j`qo>NoIZy=ka5}V)O!CC z(np(gr3+OIM!m_w)i?Z^=LV(QavhsxfZvjXD~n6pD?OHecp_pn#-OWN?>*9^aZV|K z{yyLzd5m}Inycwpt-3fNs7E@}m2E>_Z65#nUM0;G)eCbgmxvb}cg(pUisFmv$aEp; z8r5>g?Yp*FC&2R%+0E?^eM=N={+&m7)x3um(Sl~GshD{t)m*MCRwc*320(oU;j!gR zbiQX(q|z>vT#I9S){x`@X39*tdAB=zOiHG;+GM&7}pB21Tk~R45jm{G*ieyog z&}A0m*mV~2q2cP8Yr;aGIC&nYN8vu&@#Z^twi@DFRp={FJPP&cD~+!dMI}V*vM$s| z)B(SYt_S=;NGsYV{oL(AU&78o6V=bYc55z%copVj%QrMwth8V-_yZ7+at)K|coCuD zTB|?4s_?q2ItA<>X1uZII^n%;dzeYCdh!iqA4X`^ssen4*sZTY?O?P_(uJ&7E{qzS z6aGZ`mkuaG+grPt_orap{Vt3Xt!MF|_r_|O z=Sb^nd9|EsK6sBm4}5dyjT;2Zk}qHiWl&|$y9mrH1a2W|0Roe`ZYMm7Wv9UJ}^p&yk=Wo@2$9|JM%c$ z`)=d=B5O$gnRi{1llxFThm{@BW|#lNES}332mA^>k5F1@pS)aZKvJH9&u)h`puaFl zel9m@C@rTg7i9{zSlw&-gP;&CfLnI!E9k{sC4`k4odaZe?`G z%UzN2*LAX!Mf@P9MBRWm%EtxzTAg7sUK@y5*9?`Bqnb~J{+5t*s=p4Z7X<3x)L0;m zuTN2WML_;Okn>^Hjkw3lI9_LcD|+H?1cn{g1S2pF6 z2(L6g=sAS9xIf6W6*>+2XroM+kDhi7f?u8P0PAP)ejVm*yEO}&=Xqs08LbjMp}(v!czeqNnUZ-JcUc zekKL@$I74YEEwrzJ6d9UOdQ15Y%E*+S^FiYyHC3xw`i}QxqDKjgx@q-z5X$AiyS)t z^X4WVc4~dMeV{`O`7^+asihsZQ16s%MEG5$v&W~vRAi3=&wLo!wIM-a&&!2GI6rWI zVl|KByDM8PHofkG{N9d367)yq_~{^iRyXB-eL?%NqJ3M=+Z-OV1is{xr9yK2ZY8hG z0aehyYC7MpXu><0@8(d6 zc>F%wU>53Mh5pRoY`l10mIMT-4B(@hBvHFB0iG2st2zAw<+rm=oN{^by1%`OroIsW zb{i+jZ{JQ6R`))A6zut~I!g5Kp%$&LwREYb0(k#AR)BWBS-x-+QE?s{Tudf9avb6F z`ONThE$S%$$8t0B8Z3kumW98AA4uM-XkRsajthA6`XOaW`1>K?-_NxLXK&sAb$u;gt%6-2;dy_u&Sa?I@lK(4z2+H!H+CDF3+LtK zUrzFKj||k6%-r{gt_SfaMvi)>$rvL5{@!#wEw7H4aK2>|++REH?As3(w6y*_w`GS@ z5&lBKZEIkZwrXY_|J7iod~{FpD$|eradP$Oc<(CfIIwRw%^PT;9~m8#GQs@u;G<6&Q6z6MjJ!uPEpSP=`lo9 z(6hP)_@QyWrN{Wh!nq5Z%1;0vV#qj0ei!Fz>NVg6j?Yx2)O=Cz20Z6)$RRfSM6H2Ezrbeeg^$UoUu$T2Q}yP7(EM&%XI!*3gDs zj<;BHLQo%^Q7rQrpWHf7OyCRb$BuA)`xj8n4ioqUN-zrqeCl1tDMxl8^FmJ_^ z9Oy@IeK1?Q_1ChY6w@TaEch#o%+vqUisJj~#1KD=?RRCoR$0ucZq>`!yq;QJG%6JT z6Wk*5Kl6IA%as3iM|N+wyob)KUNWbZ8fFo{Sa{+d^6$Y>*<&u2qAI?l0ajp-%@BXW zr{G;tZf;OE&NV0<1fM|LZE-83DZ*AcF55#w!`Q4oWOrL|Hg}pJA7hoCwe3Q6?jhAo>TBsDPY@D z-krPu@uli=`U5o34Hz&<-rM`Fb5~u>UX*jGfJjtFh8nJU%9l&J&sKPi44DrJNAn^a-tB z^%?EFI<^hbR;Szx`hlSTU$rJmsCb*2+tdN%FJ~_WaBhdTs}^qzG`EI)l5ajl%cDBn ziCAcQitLM?el-;*vaVZ^Mf64S=sZzM`532ueOa%&i2&bAp$kp>B{kFU4^$$2(7^eo zti5@l4p&6=L>j=65D!O+h{SX_470?Mp5%Pa2cs}$q41dP_r62D8ScBVh}__fx_I*U zAb9>k$`b9qtFv`fx=S!~-C)j#TyK?4f3VAcKhW4iC_cdIvW3jJr?yTwNj$ZN^T2d! z++{&MQ}S4$QZ(;Nf~l*e$G33|Gir}pZ6*DPO-ikEsU97*c&NYl3)BN({*bl<*~O%L zcjVTYGsOsh?jq7|UMC9eU#;^D<@2oE9DJGm?-dMa34ryRuJ%0O;nT7wHejS(pcl%# z<`S+DCJ|ZxZA~yvm?A@b@1>*L-Es!M@Fq&!`gFq(yRy zAK#8w=mkE%@2`6m`q}4RkM+-je`(32cM0@y{4!tI595O-*0LSI{&ZuP%-dWj>+!&s zt3vmQoX^p3pX$cP_q7uQ{BCw*pk3o0l>qHGy9D^fEmf*5w(?1{nHoGVtDfcQ(o)M- z#MktypFsIoWlE!iHlx|4JV7B6@ii!p(l`!#<<_B<%jeL3lLHlLX>~6)wG-+6;IDeH zBYlIoSUPjAb;+Kwcqh=yYakOuB;&aO-SL1=a|3m9@B}H$(t{`?bUa%>@ft=Y ze7^foT{ozs7KqNO9I^M`=V%ik=(n8f>Cgm!G(^dfH$PUInD27xnKXR4ik4KhH^E2CH1O9&U&gC>+(Y`6Ecfn$dhw2! zZudvCf_na0(0h0$wqk$tk@@CC*w2o7^R_iTxfL&QWmkkA{gY;$x@s8n=O5}Lgo0kE zrHkLK)!bRW!L5Z(k!7E+=q5zVp8h`1pSzw@un6fx7-~U3u-4aN*ten0bZ6&F>{TL6284<@`{*gV0>X+>daO0I1^V2qkuhien4MiM8-wBZ*{r1DIu-U7 z20{Dh6Tx}Ore>&ejx!V_$5Pi#rO?ZdkX z@MkBJn-(oOCA16t$uytDYvUO8vwru6{6hB0(8HwKsGX-1d|!JT#H&NbNg{hVuGb2g3tWvKCndV+3HNwMEwZ1fE@Nd%5SW|K#wbWs1s`Ruq4*W-=ZHt}7fHqHCRL&awncmSi zb{zXBX*FI0J=O>ha3&BN9WOJz8hqu^eK)2O+S?m^u3vukT?5$%{u#yuY4Ofmz9;aH z@5I~uUw_Wt#4>I*1KA&_FXQoC;!71)Bh;h89{2}v9zQl;^{d>{Pk-Y~Y;Mm@g8}Tt zRw1)53ZQTG>X^OTb?!8Ul{rOm1isUcYTPvpTi;(W9;r44>)nMxy}!*b@@-WCr5D8; zRA>+?ql0ta07m^mLQ0%?^xU*8k?0!2HyqhV8;O z&V2B<5KkEC7mf}n(5Hr=egXZm&M$^WXs)+?erdve2mgJxo|rFZqfX%Q+M4s>d8nL1 zS^Vbir&>SPqUUFNsLYq+*69TgOsS|ty}~$1#e!@3RqRnjax=33%u50O{9{{~3MM@m z+K*XXQ4Q6di|LFY39D@0OlUIfFhR(Tbx)>d`3us6Dzf(@e zZ^r1r|0>%4!hLpAejx1y@RLAKF;|4DoWHzQlu_-2_yNI*4tT{K`yTMs*mMED>HmQ< zAgQ48IF%TK@TEYfGL54|(h9nphbPkG3U+R^%-mTQ#5}L(4d<~73-tFJ?=tJ~_Wr#9 z^OMq3VSW;8@;Z5bdSoq%pA5{Ujx7*A>&3Yg10DeNBXhc@7W*25@iBxB^^O5!Sz9}Q zzEkB~B;zjZH!rxnaM3f7I_dm7d0xeUPXy-_Dyda3K4G?F57e(Ap7ZRkvUSN;H#v&= z3Hp`nf7qdXY=l*SvxfaJWE?-YZ~N!OyJC9d(60gWbtjMPmU(OR@IY_Cth3&K-a9^3rN zW8`Ug-Q%%IhsGT^);w>(}=S z*6*zC2v@s1!J3Wx-F1M3JMOFek~=zCW2cCn6U5u5BI(3!siRi$ixrSPq8#-)_4QQ? z?0F>f@E`PMp^Gw>jH@H#*H9J@schWhZd_oUM*C|(%xjqC-Az75-tw)iXF_S z^0&uP{5X4~gCkAdFP7jw>j(aEpgNK;ynvm&e7W4BB&`sF`_bMYHm%;%(WItoP{C`T zoxN5m+z;kNxhmLin2)TJkx8>8wg)Vbu`xf?-*Y`}s4YQmo3L~RTK`~@_X`QyLt^9V z3PptfF^9M=E-Ibs;%6@6?%e;czWi8T(R|(112g{@NfU3+@u#ou4(e-%coF)+px=>1 z^7_Y!R?bM<^#yCfMm-#!J#ASldUt;8UL(?b@|e_@S@o%QIScWzjg`bTkCDdJdi^hq zHeW>bKnI_OsD5`}qm#SLVBT_$8txq8vskhx#g}Oc^l~J4kWX)O4mbp#H)I?@C2%y4 z+~hhHc_6-|@lk)e0^z>Mn3sol^LwygPMm&fSby63-e=Bm-UcZ8{(fzFN;`-qvUANP zU=N)LII6?Z8n;dBPN4e`7usJNP!o4&1rc_(79e+Q_xB*&v_JnHQZbY56du?dW{nP1fAH`y4SAkB<=2Olf4AvRG81Gm$m|aG79*QvG_8X z(bSKcqU*Y%YVvkcqY>XB-<(T%Af3jfmlVy@UYS;{nya9jD3@@Z>ZR*e3le*A@?8AJJj~AirbnoHb6>uNada#>3U;U_v zy|%wjdoJsWDx~gI#v{Db#*ZHY`M^(Or}VtMk#b+vftOm-gf*Wn}hhQ z6s0hG%Yp3Fp264!`1uf)R7kaF{8H_}dxRmpo^_oUO2ivZ^*tdZ#uM@%UUI#HVN`Yd{36Pw3s<1IeM1LT0s7q9AMti$nj=+iF_MI{;1k> z$iT?EE4oI(D+=tz5XHgD!ls2?6{`@~nOBuTO2(%o^v!bv6ICr-;PWBABcIwH?Y^;F zqUi(Jt5NK-JgrsRO{MCy7@U7QuHI3?D~qSHjDptWB-9T^PPHszXbO4U(Eg!Tx7p2d zz?Qpd;pbfC&Shhj$UgvI&`4T*(2$wR6!4ueZF6=;Cx;q0mcj+RABJe^#UQX1JMrx1_QGmaRK{&q6aPZ_?;NxC$CYwL-FTv$u?|l-<{J8_<1+$R>#E=yX%^Ct}_MUKl9*^ z^Js~say^3s$o}#xOm;;vY;sAd8-4@6I7&%8XkpVfT4V;zMuRdwyPag#Bz*r0^@w>5 zNp(!E4*I3b+8v%eE!(K}RFDq@#-^P*{3uD5zS+I`sY^c8E=|GQ?dHOX=Rhr}`!72pSsvOL(-n1@2Lr#V4twX**teuR9t zu?drWBDeg6I#$$aY@TgSa$L(Cl&Ar`=~<_7xH+3hl&B(=fA2;8&a5Fa$% zpvkxm?O-`+SErxI_WHQ>X5kB?^Z8rn-o(CB)&)FeVwwhg*xjGidmUgsW>^Ptw04JN z0#DGN2Yi-r&)=oE%eMa>zv3aN=b46QTodrExEvja@*@eq}2k8+*z!$+0Um<>|?C?g&2aJa> zoN#Db+7tZa&$p5*{)TJ0j2-En#e$85zqxJ}^eg9c>{X+Fxg_PNdkFU1!77$69(KDq zeZMVwe^!8%N_tMh3DpacCFuLJasw<#t_Q1PGu9hf!+kJ*YunOR(ti*mTwh#Ays43H zo7>9g`d8NzhFD1NkD~oym(8w+wZyHwy>LE;SRNkRCGauTgEHDGV6TwA_lOPQ=b@kl zb;yrFUn_9jPfK#e4xRbdGYD_4KFAw+CI9?pK#wyU;W=pjsJ)A2EXT%`j`*&|OIsQJ z3m-S891-(`cnIp>!ZV*g2TCL*uYCabgKg|=NwUmLS9ug=ku!F#Y6kewCMVg2%;RNG z@n}C;I_CIo+^5wGznU=QzxBd%-jlYzt{Jr0REGFwm}hLQ3u*MQcU{CC#NWg`6w4)t zkJrlkf09OchT)EWP06gis}8kYA|U-FOg1iWVshWZ*F7~8$UafZ112t&9XuF!VG`nZ z3!dTGaAi(mkMdq3+y}^SFn1~Co{lXJ#OPc7Q2)WID@c6>;={G#XU9b4|Gky?`D{D} z>5A8@-B)$hk^l9i;2F_-{THnw{<;Y94ioh9_hKz;)^Bt;M?`#n z=y%Pb;Vt$)t`$oR74)a(zaZvkNUDoH(SUpw=}R#Z;(R4 zmiggLdIiGYC_C!wajJGjoFli&BG%ti>ygeoKLPk*t$Iw8_M^eO+XATzP4g1#GoEhN z1wMnJi+#sEr0=6NShWQ8i&ZD{=c>d-E+zrr3*wFG${-@=V`=-wD&?i;P`?1>KCf)2 ziEHJd7T!(rv8TcJluWIFUkLhnAD%@^ z^eTydAB1?UoJ~3U{p+jDH@dM3^M%X0qlo>*M_hOqImEgXGrrSi^MCUj60_t?WCi)} zd>}!kowtb9e5}d~oi76&YN2%du@Xhs6H1|R*C$`B8^F*%b5~c_E1T#>O4i96|M0KW znEM3pgzpD@Qh61k5AXJDowh!TkLEpEN(&3}DicY>F=Rhj6>na?S6w3F+U`l3^`z_YeT@cnUR@Q`3n4xv zsz@~VF!;UA+HfEIwTb82zON75i!Tu5n*kk~nzDx!@@xkd1o+Lt082u2N_1DP)v&;x z23XPRY%Z|6H;CmSy$iimQOAL)AGq#@EqSmWs9y_bH*HJcq%2QC`)63sR@sJEe7Z`_ zEf~em^G@`|bjzYU<|`zgTSo$Z6GX(9J^!ff?&^Ra^VWy{lIyiYCGF3v@hzUqH2t-m z?HpxzqvV=ESxHZ*XAEJtGV-I82h@FQ%H0uuW#C+0?CzJeC-hX4AH>JpV4M%9RHI8J zq-6)#Yp@^TRviDeB^ufz`&OZRgM5ZZQc+1-+%je81@pbI*}!emmyG0@Y1Ah#BBpx?!rW2J-cE|}a$j2c zH>?R`5+-apZ6+yh-_tY@za0PnXu9rzChsN;s3;)VTE(r1%rLad5D_9yLPS|%ic~8I zXsCdoh!~*Clp!ipWeJjmu!OLLAXX(rWbYAel_4NIj2PbUe)(GeB_{9t8+Xs$J@*_l z;DCl=^QY(Nd=0ZYHF3D=ro7zbjNQmS>OGdvDm~X{WvpEH&=%&oJ2W%fK+lq8&y~qV z{Bq2O$#%W6jopH6yNlv4{ySdF{k84)XCE-gDF1}{o*_BzPmJn+Xyc6TYgKqjAM;V5 zF~9%hnPFX$>87(CkKlb4zo+{Ee!)j|VWp1Zr%3@rU%q5Df&M>>M|E%S6fl+?ApVDZ z2<3&Qg(q=m!<5d*FNwz^4Q=9v9-{tk|3gg8&BGbhIe9r!h@a19k6NJpGqCsb8?9Bc z)4W`R>;d%ml@!a+PwyQ2T&Ic7ALc7lZ^hk|-ylaXM0l#oT`)SbcvpQm;uUot=*i(% z{Y&yj=y~#+ z-0m*8HqAnPEF>c`m-f-{xn+kl;@4JH7#JEZ74-AZU2#Kr2FuQ?E9$71nc6JPfc@fC zKGqZ?_HV8vTVHpD`6O>#&CvOoul{-Oo4C zyfD!Q&kKBH&3dajiCqmn6@@6?!TbA}vp-~2Huq{t!+Dz3cl$csb5tr6@`)x0FR;3( zVlZJqwODf3G`eqs@DWRTQEXE3Ey6?izTtq*;9WcKJTG*#OPM>e>xwbrAFQ5S0rPQq zdTIK9V-wi6YKbVdE5DFvd(%$!LjSeeyZ*x^T6w8zq6j}#x`aq+%-z$x5Vy|=#an*2 z-dYsvoQ_EzJE#Hvx(iRuA{yCoJDAv_9O64ur~=mT(%GM)svXXNJ)0ARxwX=05gRuh z+zs`Rmvxx^i8t`Cj;z7w3GdS;O|=F#iF+n480G7_D2Ft12ej^~*VXQXcpLO;U2r)T zHg>z17a84PUq)CZ%PO>BSvwi#q^6J$dvaQ%RP(_56oqiecb=N3*mS6>(VKVi6sd7x zyhlct&u+Q?_u|r>!E(JFfbV&H+wzwEIg$k{r3%`?e(?NFObOmLtDX#}9KP2y0{w?4 z>$T=Fx(2^62I4a~Pa&EjX*rC-ThFWJ;5;t)!~Ab{gR!iLgv+!U#D{*#>qu3w+l25Za#1 za|ZTfO?vKC7UiFJ!5VGHb#MhqbfgKmu&tJ1#v;8HLM$eHN0Q^I3@w#pM z%A{gbzRnjk@1grJ%p*F3`Xx1KN7{!f9%QV(1N+;JFFOPJ=$pz^72H^8y#li1i}Uhq z_i2Kjm@DwXNWh0c_VOF(eH_{IaShUA!kO9pcz^E24YXdE&l#WVhk23ey%{DbKBXw( z93AIiC`;d&@2I~WZ(~B&CQ0qn+?p^}0OtYospmB04e0PI3q{a=8PNT>t5y{dUVcCS z9o;uph$N2Iwq;$*?xYgyq@3ss6YE!v6D&sW+q7M-TF$UO8?S$bd3m+HOi93xLzEGX zH*U?pTzE(kbRhieuWbUOH`b+&No)_a9d)g^klz8IQ@}bgH3N8oHH=rXnDR7gR;PXM z0Dq*v2>k~z&*_(S%}qO}nzX^+_?U3+t*BQmh&zab^&j+OiUkIyuiu-rc{i%(b>Xwb zww&M4zuSIKDXRZWU@m!Wn8BsU4%$S?EV%<6JY`imwBG^tC22&{Q#yYUHS5eiK>veM z?bKBBo6Y2ZW&uy=53{nPgCjIA?e@LMm_z;ut0Sf7(4R~5w!pkdh-dt=J4}o+yEj#_ zbDwwoP4c~X(sy})E3&<-@Jc7(v0*&V?#A4ln?n(~u?P7dN~mV;hdX=n3m^-g zo<=Ue5B2}sas)S6pT@azdqo(qv2ugP4B{KZ=ha>ty?zaP!6Q=DLi_guM6JIkN+?=kl zMW{b{v0ZEDL9>{e_~wKwi;H~zZ$J6Xqx=NRRpa~}2KR{9m)>fWAIH0A5jc6@D;X>K z45;4>V>(Kiw489QIE5?x&D-gqm!JygfBsGst-%MLXO4f+{7}o$Jue8Sugjx&gOUhB z6k0M`QNQ;>JrwvnHeSH5ma_6#CUepHEm9R5d@MT?Z#S>LgZ4xJo)UwZ8{%>)W)|Wt z_bufJQP&tq<18BvP&bjJ6XngCx8 z>aCg)>W$mUtik=F@IE{PswV~O8$Mg9_+AzKuN__8PaXIYw>iYq*Uhe@`XPbd+UFt% z6FWQLezGafc`AGIJB(X+QK=~2p(x9*h|#=$gDO@_4!rxPWrZE$%!1w{&2mTD#{q1j zGojl(ozVMy&|VzzU-Tp2w!G@OzmBQi4fhB5I$?~6;t$7CyVyy7X#epEG>f^;y0F~! z`Gs|-ooo8cI^K-WFISKFZ5Bs(l^+t_7N9avB;74Di`G{^{N9FvdRM&`$LiOR-A&d=9?LNg!mLV2wRA{0jV@_t^|8;Pd%A;kXt@y$M@%`EWL(o$%b!+7d>yE#PS1#)50tKZG6nXD=a<~CO&8lox^crM z0o`9LJ9-~sl;aZSQ3w00{|&pWInqpRD|=8)ScUvA%%f_$-PyE_sWq}6J-;78nr?DS zRC=;*`#NF#@0-!0#r%Y?-ko$G@Wce*yXLc`X6q!^#AA;FCU6cB>o1z@I|D zRr4N(6FdU?$(VsXu6)ci$;@O$2GqZM(4+zOKu1ZW?RL9N zvq{nelwZV`M(3E#{AryNe~WIr$LARJ*G<=7zrNp?_LPuOl|K^{z%Rw{t_t@I6N-i{r)IFqZFZeH!u+30z10jU ziM3QBfOz~p&mhNl%p_AP9Q0&h{u^a7d_rFbCQz#Sm1M<2{;ZMG!Lw|Qc%&6q{ays= z9SGi4jEs&3obWP|b%Xbx_djV^tirw@>sLk3>4JHue;3gt2VtJ6o2AnVz~i$e?};uj zzKgqQ6UZmr5}hZNDA`=p9F92TpmL{O))z9wdzKa03YVZi zX)Sx+DO)2~-AB=N#~C4B&ek#ioUR+{D^VT@dSV1p+s4PBw_GmMTuffGKzoy|j_`tj z;^uX|uO_2js9(4?b_M&l)Hiy-m*E9G=J9A@n!oz)G2o@W1btO()XulL0Jjn66u-GC zzD?1C4gFJme~zW)b=ylm)fcC8=!1}7?QUL^aZt(mm?H`1ZOkuM;T}z00)Gj7fDoFQ z_uE4SolC*M=Y#Z+rYVOVe)rNt9%~=0ul_A(^Fo#2(14tz1T)B=uzYl;t=f-Ojr{YD z?Ajf-|8JP@jOw}h=FXw${OX5ijdr~{mwJ1o82C!ypX`iYx7)Cj9<5mo=kYzy?*^}h zo+A@9R+p@R>POfu0!QBM1=N)Mk4OrXl6Pa<3Wet z|He9*XVIGZmvLC?7`p)QxL@+?u0Cd$lt*&01j>)`nbCE%YCfyZ|Ev~-@J^NQM=fTa zOvdq^>sOFH#+*$o9V{hOAN2KoNB^InMCj`2=v$Z9qooMXKUAKT7roQ)t}lW9Tw(#u zuVo}XMg4%;7IO)RpW|0@#tiqPK|-Bo8gnFGe& zheFK(kAnVkLmdp>esX{3j_+7x~tYx=0yx!_X$M|W+ppG%|$KM2aWct## zRcL+neKfU6PHR5~O6C-pA-qT_5Z%MWaq@Vsx$qN=qEnYF_GNj^brXYQ0Dm+-kaC(~X%ni!^oeviL(g^jWq;5Xo(fxu6zib5|h0p%0@ zZYkPy7VCsp<|I9Z_-Yt`BbY`z*PT_Kl-gca7h(Q1ALTb;UL}EZZXew5a(CyMBHCT) zWWDZ(!gwOo1fEAkp74A5!2kXK^lzEE>D#(n3>(mS5Af5l(9u~N^T1)f9Nb@s-`rlO z@9wT@?)!KT-8V{@m4`uy_;CD>Fe#ZVCiG z_}e+Z!4Aup_wOs5V9Nl$0)Fy!nBO+~{^0%Sh3u|{`0DI&VZZ$ywwEOy-A_JKENz&c z-_MeYyLyCrI${YVffUS0OYid=(YzAiBi=hhDrxIDp)+_2#ow5sJc(qJWYGH(kLsC# ze>F?ArqfdTBd8mPIkZzLntsL5KS0TN-un)nN605n-f9<(X_z{?7tZ&5rM4JOU2{id zds09*il+@;937o*k$LCbhvsDgUdaAP+<5oV`alsz!sN6O4_o%;V@~BGd*{#{7$hUuBpFzKt&7!_O8c!+q&u!Tgdp9D-LV*#TRl{<0_|cLX;8d=&y`#u4UW zP6X&JE@G<(Vij`v%q9=8C$~l%H7vQa;x+|<$H6~@2B)bMN{C&`d^!d84EnP!nA`R4 z1o>H(z#`Q+zV`(gG|3D1^O=|b5a#!MvqKKghJ8`EQ)`dx#iF|?#FriW2$mnlCa*9T zDa$kd{W>O*?I})9wp!$xb1m_iUx${fKl|+j{ukt&B zAKgd&7abH%#P;x#pLdnl*~Xkone_b~>btjjpSn8W`FH_7c^TQ&O=YLg|Gfh67se7l z$|jMvMfQwHbA|ree|%CU;v@ZOp+v&XFL!I5*M&bp{im-5)i+T8fkARqO)E|;zUkE> z;AP+^xRZ#-uCOCY&P_YVTKqSUT5>~O#ExfRFB&ldRfW_3`UlKI1^@7}j`E@&@*h8S zNPPdoh5OC(gMOsEXD0GTLI;)7jDcA*-)e4cqoKUQYH=w{I={~{AK-zf@h9^ z&;AIXAnKSBM$`XreSQz&S3#&ey;$dj(r=MGQ@|e+l;rW@oZx57sFo1O*SrM2N8|E)w$Bpf~33Ho|_XH=y z);_Gr**ii*^CUz^W@J%3pl?r=>}c>SiBrVSA%E>>C((PyDy4DieA%k5x(L%GhAo;$ z^xx7gJ-^i@LKjVpph|Jzsh{g6TSmru%|_07f|6)F8IejoTxe!Qk=8);i=LsLX9 zoWCBdBs(KlR+-Inml6^33GEqy!-9<}j7tP>sGkDf%KNcm&8Kxthu$jS!%kpHRgrPrXhM0i;1HPno zabAWv{S{46iq@~($NdeR#L*j*DicBZ6McUME^j%WscIs==z4-=YC{s{&nn+}=3j;T zKcVNF9f@m`c_t!NunO?vkY7n&&hu!KV1-qaB8I!%Lej&4Up2AELSAuEq07p-$ld(L zec*dm+jT2t!SnRs51E=aq<2X?HP>r^cm?#MNE}95o04kaAe=`_nCF(2NyjOzmKLy? zke@n6Y9E6BeXR0StD%mtzNw>0RbxmNJYYHWnnAp}_}l~PgAeq__c#C^f_excD1*l4 zgj`y~XF@)dSLvoLtEn|B=xvvUd=P#FpOqJw_Rads@w2`#uQm?!)byed{>e1q?AQnO zibZz~j`@L2#iw^?9zc9O$S2ZVTScQLf0Awy+VA*UdAfE3V@%S}8R0X0S+HFDfCqm0 zQ>5pxLTJUM*7nLVy|v7rJ(hv+#-#ercX*#$P?X>z7D1I zJDr+-G(+KQ9T(Yu|M+OhJy*C|@ym8%=svNmYdy`Id#$mrAqrsc=lp12iMquD3hS9j zKcsWE@(`oWvP4$LvnV`4XuobTRvNZ99d0NAe+KuZD!f^{l=hHB#yN zn7X6&xp3YF%kPPaiB&i0#CCSb3+sgujNt&%gpth8k00zq{GoTz`x~sAH#aoP03Lw+ zW`yZ4`uh{O>@O;^XutSw_vA~o@2x5P!H-Az2YfiYnJ>u4;?BW*6*yl8zBn~gmdVaf zqMPp@Nj6ggt_DV-)#o;@60tyhO8@YnjVndMx_cG%$inzj`DnlYh+UQ)xzlDc0{G*p zE7A#`E~e+zUtbKY@tP^zWQX!A7{{;uF!%>H>z4WNnJe7%9&hC43GGpKJyBcMWc`t# zGDl&&YTMjnUq0Mhr10J}Ffzq_zvL(eR*te;WlpF+t)KmkArmaNM~IJLzA{6F z$YBrmDwcb~dF#ZxI@%uGx;CRb^DKt!8Q$jPn>P&vIl`XuCEz=CS|;>)W6Y zq3yF{h`(F=%pInU#=+rWvO)jYx_%kpFVB zes`pLQi$i~$SK={;Wdft>#m@B^j44gLumhD-n{4UnW=XR?KyxKfN#6$qgJI#(>tbz zKj34~*Kju_&Zx*3bhV`1ItcTLXd0jg1owUQgD>x8k-x3FWzJCJY`FGER%JiRpHV8Lf6Ng_pDrch+flufZ&w&Equ%wUbbC3> zpQP-c=W@x}Obfad$+;uYifn1B_;~`V#@NTKEa+!|d=wML3l^Uj`)#}6Vc42K7*8K; z0<{N!%AfA~R}we-v9`9nb6A|eeLLt*XD;FgQd~51i*1{?HgJB_?F0Q5d?d*2a4N$S zu7%&KNjxCb$If57Wae~jZPuqlnT?Ckpua+-jw>Q^4ITopKlYNtrqSZtD+%yILJX|`^gkq#HlCAMDr!MYb*S-kve!k#VQ|pq8-^hIXQYe*vge*jE)zH=wo_YX3G3HGxKvr%NjeAoVlv%|kZyoymS?{7AqGDyTWs!M`!`M5iY0Qn#7-)3c^S8h!V7S0X+Eq9YM>r3r>V(9DEQajNQUbkE_>Iv&Vj5i5N%gF5CRxZbINBm?8d#=4W z?OfH=#dQ@3pZed>q#iK6+%UpfV~YI8jS?}DmEuj=S3`Ktfh!+H)&f5pqHqd>=6NB} z>cTP7dYwmaVIFi(v8H<3!7pIDJ3Y^MdXvmav=QVV^k2yDdFixb#~0Bjcf0CnMbKX` z3i;LByV*Y4D8F4vpo)miO(~pJFc<)T3;nj~5$D@K8ejTcW7xHleAvzs^nIYd_vf^( zq{>J{MV4C542rk%jYnpM_1o+STx-cY{U=95A49%pqVlnOr;KQ=>{ybqq%a@gQ%^MK zuyx`Y<9zUsbN(ib{rj)KkuK>n#)&-_&u|`j49|T3p&skTZj=we4rZ-TEe5&mo$HO^ zzD<-nXw;DM&(}WCU+q7Zw`R}nTO*V(PT8w#hk%jCbcGEyh zN4-`j`9~wR56kan9a1H7Gc<)%yM zzKKs_G=#UX=d_j|ZFw=qU##O$eid(`0S~|)QqV>|#3sTRx#?>6F!FeD)5%7`K z10;&pO|M}eKcd`d4E0Ki{VlWlv9S;JLj01P9vqQU^JBobHyZM#^hE5~v-Ih?_VVJ- zP``%z=N^<>+!jhqd{g=)@NA@+;h_A`dKJ=#!(7cpnwyQxxSFNpCJ%Ui>Ta8Mt*&_Tm($;4z5k0(A3X|7tb%;F%~u24 zCDEOCO!i-z|H}g9Ye1h!I(VAZgnK&ZVFthFUviSeU<^3L9hE(S{;p4;PM73#e9=gq zmOQ8xYG`AT1^i>I^2LqTeX;l9VTJQ(n^YKE^X<@i^$V9aTe4n}I;K#!%9S0wud|@+ zILd#IdX_u{yg5%P%le0CX`LTS*Y#hskWrDlSMN0Hzcrt_bsO?MQ19+AZ*C>T9`CW{ z3-P3{g(oTRllbE!`Exm2+-g#O=4A>N@7xJnsyK=Ei{f10a^2@=YLtQlAL$Jl_=<^a zUMB4)kooJZpN2&|9kvrp%&>M~-IMEt{_D!mB0pU-=ETk0I)FcAvmT>t>0BBjIVqhB9?bEzgy~BN?@(-%b`x4f_j~dd*icNjR@P zcpO=~IIUhjN0Kp#^lT}sYMbZgPCR<-LFPjI+J!N#-CJAl^|2|J{Q+MtRE1;&N93J; ze%$36*LdE(YOp}((f0{|mRY6d<}~%sSR=-g{>aIE^NRyLyXgR&ck82(Qk>dnHH5g8 zpbyoEeYI|F<_x?e@95psWkoh}m<>CC`k8yp{H2GrJc*8vb+mvVR9+vTfdlBl{O;z%zD^eJd!}kuT z-~Yr-JjBoN{yH#U*>cl*(-JaQ=$yzCS`_#g3PE607eJtp4p@7Q znXZHQaLAvyC2)~qAKKnh^(t0<|G)TeO(>%k$AjOit8yEhd?i8b^6FuOWxvKt5nT}s6;;KZBLzQJl(kZ zmK@l>iGbnHpM!!nfqqfVDzjL?8%Y|P)zv$dSFJhAHG_WcDtE>*D`wSCOp1*c@N+?L zJ==^kCnizy0 zLU}$V@qODH*6;0sdSN3zG_StJ(Kcp|$c>%U$V2hq2zTvyE@fA+>m%|K9G#3#Oh~@vrC;|V*t+Hzt1Z)(mQB7Ghr}IVEWB9Ep^bf2s7Eppk zP(4pzXJkVX*{N4{) zRj@sTdYA|A8P4K1rAchlgZr4zrr3V1*=PIcB!6w%1H>25i_fFs(swPw@I(oipIS`I z4Wo@Q`{<~M_5@Bt)|h=ts^VmScmE(ER&IRG08S@InL6J zIt3q?=dELf?87LR2KG)3^iP!-+U?)>8N!$(A%67D+uyM~w72Isjds}2IsN!|rk&A% zHy$SU3;Q2csS&~HYp9A+pM>=~iiy9HWYc4N`<1AkHXV?^LHj>Y&mI}vR>X-I zGcyM~5Yc`J`bT<8`WVaPZ4Y9P!3F06tZ}xS);`d2uB$Qn;Re9(YHW&r zdy_F;S#A02DOZ?(F`;Mjv(=^zG6hSeY`{fhoD4vnbMzL6Aiiv4cv1N;ek+jQ-A&7!Tn<#T0q8ONZX?>g!yYUHrpVoCq$ zy?shaxIN^B{3p~G&CN#%&rKPo-eul&VXxk91nbd-S!Jg^kN?hHzAd#J z7#6wS@ZWiCF}Ru5Iz?wwyB1lWx%tI%ak&!5$$}G5Kg)!AJAdAzX9K!VgZvUnx!=+Z zf88~9iWh77A0DvYpDv~GOevw`UP4C@-~nEFf$kr@1#tiTUU<~0(z_lO5Y}9BWk;MH zDj@vX&tIst;biRz({e0xX?;)U%}-U7zhaVIkK5_lJnVS*^`GS`_k&Ml08fJW@<-}R zC%a$lGb$nf!Wza#-YRD8-#}pdUROf#u^`0ok$U^b!_}iT&WO*VXJ4{5!yCIMw!IQB z#IJT#`wrSqrg0pPsdy)AYNsyNVA}WUSboPr%-vFH-~@T zrj*mwdqWuixD(V$2a@xZB+r~Czk#{s|O zS(J)tl1m5U@56e{vqq*yUf$o?yx(^U_+_sGSaH8H$&;Gd3;1OJ1AZA{+0dw*21~QJTtOxH zml0Mnq1}dFye;A8mK2o#^=JCd6g^ra@s|k85BLQW6gSaX4Yht9x5qpqVLW8ZRsnrA z9`{Qf;D`J!d`N~>K)}x_&A(8Z(RrrON;W)g-)I--)B1}U@M9?_TbgIEUM0fsQcDPL zvu;e!w9gS@3B5W4s6W6T=E)e*EjGv2Z3=+qbxGSr*xipGb*9-YUnBUAuOYgG9z-pOKG`455=cKek_|Px4VzWMCZ}KTFJbt zXQ!)}@=yiXzxk?Cc}4C1CVOf=Z8@hame~?}=>BKHg5M13&F8>|TMtxMI|5>au_jZc zkZ(G$z_Rh`eor34tTfDz+~hfTJ_dnpNnY@L;bQ7JJHheqkT)vxIE&MLqEnn z`7BNC4i`6Nm)}u5MhR_sz33<<@3v*65}kkj7xlG_lKL%OFi#ofpB7CRa+3kac7E8y z_HJr|Np6Q2fy<=TlT3=fNSA`oai{j@kY8M`3jVQI49?$N`FnY)s@i-%BYf*|u%8oE zn}R=o{yd@QPnF;$Dv*xBeB$duJX@gy{`x={1?K7O*~93NzHyn`1o#>BuGAOJyKp5R zmk4M-4c>jnXjJ8Ko3-oRYA$`)JOlOHcMsRjtoRVy2NR0F`t%JHDdpl<*&i6Xg>|gJ}*)>H{I{Pi^NOd_|>L7Z=#_Qx;$Xz_ee|YjlL~kG9k4Dy^e!B5Y>dNEG z#JFLt$P5Sz={aOzCGbb<|3;Pz6U2g7seSe31 z6U?)t6%`CWsBDY*k)_G)O8UHoCSc9HlEwbBfBWOEPtPf-l5O?ye-x=@7ure zIkWIe!!p8m`q?^%^Tb5f#JZmH9%cs@WvILT1NlBmI+ezU^9}mHn#CQt=kl9d!@z#p z4EjIHyRS9ZG3Ke`c^2*~7xT@?!If?dPs&IF{T4P4V{t4F%SJzYl2iT=;k|$td1~~u z7MZA~4JV*pJ`te21bXSG29IBEkt(Z;Hcy?fbp-y0|3@OhAvXTM_%ph!sY^8h;%zR4 z8f2|TYG#rLeLavpz+AF%t(Jeh-Z|c^E98URB6+tObF~2fryzZxN_Q&uf__W~+pEXS z>AHF9sgy3#qr|cU`pvkcfWE~&s!;L&W6SALw zySc8%^VxL^!_lTs|M>qrVvCuZHj}4?^_7v{4S8Jsy;@fh-uH7WG1S66YaElR4osAB zW)52T++T+JUDQ9{a)jq8oW#sMy4xl$|6l{)8BN>HD@TJCKMyg=3O|d39_LdPkLAXR z_`{!T|2!O6d(O;YwZjA#?n7mdmJX_4L%+s-4bDIu#j1o}x{-T!@{OgJCLPQt>8q4Yj} z=r0L)|3;{XGbGcD)=jO~mqz@@@)FG%QI#Ke&-7|Z{C_?lv0qZ;xa>0iln3|^&|kq3 zI1MM-@~X)g>JRfj@v$AB@IkY#KGC#k4cxy{G2Dl?p=J#b%~1& zIid9m@M|bN+>)bKB|!}Y{MpU&>o+6ldJroeHGb=Ycq_rf-tjx+hveAna?RlLmG&CA z*1q`lcRxEOpyyi*zff9z{nLwppQM5iUdODho9Ttev^>7>u!EXD=KU5O#YlfdQ|zBt zM;v05(Dc}K&*h$9zx94B@b$xZrb;hX1@{iIJk+gOdDYmTxx*1V zKfw1*V6ql!oi-Qf6&V@>D4#&-QhoCeeK14gyYV>iX|I-JGk%&TByRI1*Cez35U+y0 zQ6ql7528^wV!i(3lLad+(#zujzcjLLvP)^S4|Nr#qu`(EpeOc`?dd6fByRlw^Cc_e znb+Q|(3f~7ni7QOcU1aX%ZAX_=3E<{#Wex`q-@ghY2VX*(!BR%du)F7zxlba&5ZS| z66Ejoi6iE{ecxiYebaG+de?lVb4OFX=DIlY5wDe#rl{WZr<|MP!0DyKQ$}e2_-^cC zS*?ZlH@4{oLH@GwCb!gHG$b_i<9L=N9?|0T5 ziZAslqKvw&HK&yeh5x@eZ+LVx)a8g_)plNeem_>WgfVAK~MdbH))rvxu}KQ{L6Oq-QccXZ*FB(T3{hxI$e#KoUQ+TQPUJ&W!~ zRZ{c*_RgJ_UpF~ipajb+$JqJuhz?- zO-tm%C&Ql&*GZ6>kD6SFNRQ#uH)E)8(mU~+WDXDT>MDBX(fn`FXE+s2O=lAe6#AJ< z2>%3}=pd>^+3qT7J_!3yT&U6xiO6M|#k84l$7@_hZQ=PT*Ob#TyrxMXfG^3#Hkw$H z+Nn`o9z>N-y9=L|Jpli91o)W=c(8}x@OIs1^fV>vK%hkmT52KS{j6QtX1wVm59 zSlL)Xd{`-?HdMBC`+FaEW>f%|FV0@A(XujUA`qX!2_;So1g!CP3Bb#s4@dGn@I-b+ z*<7|htjFFiR*3twWyzWfazHD@x2Fd6?xFfri)7RrUsO+-XVsbNmf~KC#l+zV1;D>y zbwW0flW_9npx0h_o)L;$&^=MnzYN->lA>UqZ2kZ7MRxZu8#fq3J__~^XGxq>ZQ%J# z^a|rI-S4>jhh>j{vf3JbQ=x>Na&{zg1Y0`f+GO^%i2(lIH?P>&2HikY#^G4sBHwycqSn<|v)&W1KK4?)P1D|q`pCu+HE>mx99P`Bu z@F0f2X~Jo37Y&TsO5mdNCU}t-D98Cldde)aU?0-gQ=?j__5YN_=&2r$jXm*izUjNb z>&t%N>y(0?3gmB1jFo)zZuo;<3dYJ&T&3+0WU}fH3&0D|pMz6%b8~zDv1(7L#*wX_ z)??sr7gg`$Nudm9Bt{p z)W&KLdBs}T{Tu&@J^GN@i1K+8cvDDf&uwE^v);KsLf36-X2 z&y8fmSwTJ#({3qg z=XU=^{+-B2^;U|*DoGi`(}wAmTB@iYf#NRx+MD#E^vs_su|oclxLDnJZ#za0ePRjh z!vbYfM*YR{!;LF^Q_C7YShnC4i*B{-N>H_T%g*Z#)&L`pyCXAnDcyL{Yoac z?Qe+3z+ds5t=o&^cOO*xP1i!7awtr{yf$9xDkVI%%=fQy%rsOZnDuC|_@J~$kA zFhMg3)yo6s``=3 z4<79Moc|$dvD|?L*&Ft1Wr&aBf4QEPMo75pigDb8{Cl_E#QuTcorTHPCkin9H&(nm z=<$gbi1zMV2lxZ{s2Y+YH5a6L7Z?ROkiWFI8p=d`%b0JSYr&t4U@qdc*_OWhC7C6^ zpz{|HLaU*vjq8=PnOqRY1Jni;TPMpE`@c=&Al@Cq+_Ex9`RVwVJRbNXnAfh9aT-IQ0IQV0(VsEX}6 zTERH3hn)x5+b)6q_{7AE_LRP`D99h+7x6Z?{F-y(tz=$Ya|e5~h=m&1s)+Q&KS(si zQ2e+UFB4&ru^qj%8TD8qH1kY9hn^S1fP{4P7`yamLc%>REsPaeJ52JsmyOW$!`%G88CuX#2F z`Cwj^AA_c&b0p)zaus<&0r;<0p@1*=uWD!f9pHW00)=danD{h}r3~psz>AA8-w4?ALo9<+3k5C$MklxP-=sCRr5!ivjvFkC|N)6@~U6 zpCFDSbzWk$C&hH5^{#S}k>ilE`t^6l9!KY0&#mhGZS{8i$S(&)%^?24d^)_%BX;&y zlz6X5V|8E?tWY~088H!yktJ9kn2!SdH&yoIl_FdH5?qfvo*=cpg?xmda@1Z+Q~s%I zw392$N1Q)8t#FnH=NIN9+B0Z5eSdB1YhsP3=?Lf988KUC9IPK#YaU!hZn$6}E1XXo z@`dVv@&)B3=SMPea~nt|c{S+#Rl1owl89SuJcee^fPI=1botEe|8@ULlxslwc zci0K%H zr%jOW!n8w*TBcvq#vYH+g!!~knW6&M24mM4SC`EzGYjR+>{fgi%vZ5TM(wsHcRHW% za<0ba$&KO#ueZVbEmBU__pixG)VQPW90PoCus44Pi3YCT@z=Iqf5g|T^m|)7YPl(G z>I%w-wa)5?SbOyp_q`)WtXxEVTfsrUnXF9HyZtlRtDXXyCh(O$)M5BM4Xxrc5|^&u7V^o$8M5lQKaC&ccJEBI3O(guegoA@ z4Q@GT=OI24M!gqufF4iQd{ry;e}511U>Ft8ALlNn9Y(#Ky%!>We%XJN^q|@d>_wGN zwi-S6o02Zmd| z3iC%nRJNJUrIWrFFGt_O=DpK5o(Z`G=ZnXuWxeTb+ZtIl%yCBd2Xhfq%f0+BPpOd4cm(_x>x@XLhV#|09g0=~9vSw}3JwbToT_m?{Z~o! z`;;3ZN+bKI-zz=Rs=4kY`NvUjO{ z>pG9+`gQTT-;W06OsX!5zFP(Qh4tCM&j9|$ z9rnxtobdPjn(4tNi$~8YT?Ra<+k=^q+=!V0p=2@DFyxOZN_SvK1Xs@XhIj-XiPSDq z=@!mAY$UvV0{0L25IFU@J~`0$*Rz22;8m(BYHLsSY;Av|`F}ms?3Nkt=B6C-?>BCQ zZ6u#?Za!Hl;QLj#_4GnL$Z}pUOEZ6`VXRe}vfde#&t&=58e6vhDek7wz0|c3QT;q0 z<^zJg*xec+jXuxM{@xmKUsinVAMP@GUW#6SUr*i2kO%VNbCV>bCrgBR;rBI*ZIwNQ z@w1JX2}V!WA-Cq#)eP)c?#D3f`imQa9>*TQ_b{J4+-SzjuQ{P-yaVokBj}kQT_rn9 zs4N&CT!-?90pZe;MNF%ovP-&krk$vaaN6 z8l(I^+7D`dn_*5@+~Fkkt#=#v0k>x#8OQ+MJ_5B_-p^uko=mZJMk z%yI{AE<_y5);xH63F8}1Ln}F|A6C8lm?=+7Oey>Y-<-J8S;wYb9PHC_mH$qtSEBrf za)zW9=kgxeZCT9=FyAmePuSlTP#jSY{tEitO`_CoNRiu;(l$K<`%;80fLAz!dWS^Y+KnQgKbP3~B``YJOIvvd*f;@ zBU={d#wC8KkpVmd{6i%ZHm5c8U^H`-8<-Hep>{L+G;qSyTCbWh!Qc5g6IQ1ND195VO9-1%7W$9?*1ZKuDv*wj9W^J&(_k4*Abzaq+s5}AkJ-^I1-pFGU zNbQ!+j!)43V+z)mg!K2{a<0C!LHTZ$P4pvL?z#fO>i6ZPb`mQYGd@dhlc4W-t`ZktH=%B{ubvnsfh3&>r~!;wRB|% z-r#0Av>tv?pS{8si@Cn%fQ<0`I)5ydy=Q{^5?6sUQwM+Rf;-?n)h#>l3;B%g2Is>L z&DOx*!m76}Q#L_;1@yx%P)MRrG@Sn4eg5Jnw`?xeXPGr0C?+RzW)k*y*#9x)R}z~m z?#J9TL3qwTi&t`&R=YKK&kfC+3O6d_i#j70=lTBA)fRRT-w$CS(fcf>@2)R&CU>Ie z@eAp3eM2{R5x_CIkh2h1@$H)42k`HPuRpg(tU2yz?gaWO;6JOq_>Bt{-_2k9W@LW#vhx6ECxp)!rO6 zy8`(Ff~h&FokeDDQLxm+p!gD9K9x~3OUpt;9tR@;Q~$NR#{H6 zV_*KquU{+Me&B|O%Jzpenp%z&;_Ll-sJdFHCvcKxqHANfgSmw$|a$hU$&O=8sFkIzfGaV-h*m-t1>sl1%9!cI965rt*+{Q99LS4gaSg{wh{ zW5WD9&D648=V-QS3BNaN#`^H($&HUt|J~@Te{A<*%5_)?jzpr^DRyj4ZASGJ3Y)P8 zH+m(iBYFP15dYU{2g?;6OC8udr+DOZaVfIbW*PGBB*qO2!k<-kUo+|SCjBX^3f07& zj;39fB#q^v#R|>f!F=cssJvt~2>S2*>Vos;=jv8EpK`F&zK-g1_1s)_RGwi=Z` z&R?dee!H~`>|GaT65xI17%^hZm^BOfNEmkN@7J$iOKXXn)N;eN2Q+*(w?Xxi%F~XK z#}cibNlumrK{$C#A{{$L44ltLhe>V!-6=2@m zve5_S$Lj-1)ngz3$FGUcZc8iohWiY9N>nP9#@dpQnDh$p>>}jXUrX(l3L$UVg7k|k zh6|}ef135$le7=%Mc~*))opd*eliy|IIOFKBkPGw=(`?4KeCFCyprDjq*QONuHI%6wFUpI~tv{Hm3T^ zkYE|7)&|KOXeR`15rc zu*VDd5o!j}Y1|-xhl^X-+LZUSy?$4b|1kzW5S%yA&@03nY1=e za)EK`tQpwj%6EoZ`}dz*PyXlK65MCz^SvYWh;OWDN|El`ko9)^x_Tn2AJ5D7h#|fR zrmC8jmPssF{-R?F^@&D`z3PF08{xhQDM;@if|;F%^vI!~f-`p*=F=J4Hfvvk^YF^Q zG#4kV4_nK>p=;Y1H2hK#6dTv@Z$9SC&MvCk*_^Iqm=~FY`upy9de0nA+#JfX_Zs$I z&QcXenhPo%{#^BCydS< zs5Hk-&ee_H=A@JP79} zN8SAj(#L6^aZ-tW^xyeQ)QS%nf&9YzLH#VT<<;VSOxKgX&Vi8bf4{QvuW_=f7wXrjB7LN*sMJ0adC~^>74!mqX}Ho?eJ4ef&w9l^ z`mY}IOzy%CRu%ewK0(yVNqg>O^uwG_b(2?**^q#5I)N<)-zm9YfW3fux$?VrQ&(k} zUg}*&`BeVadlR6r5mw%n(qDjO)e}O(-v2); zh0y7ubd^x%dP)>AlCmvw8zHGvWTZk!VyNVv6ru|$%w|YTT1<5+vlM2oMRimpA%YZU+72CeF8H)L$9tp-si`GBfr_ZJkB-!mP(G^n*p)7CXnWY)QWY(KWm< zIbX3)SA=PB`SZjbcs~zn=xmcWRc0&$^!%^7884Qy%nAQygp1)MJJ#>~Cc)q0@F85p z7ZTv~s11>^N`p%|8R&UxiFn-hLs@D^HVea1e`Bq7iP+AWU^*nD_t>;!I?&Z5`nOoz zHn-V6LuzL6^`?Td8L)4^FI>fQH$Z#?cv5UZ!MrxrJ1we^Kf=7;TANKeCN3FLpN6wg zJvTB}v2BuQ%}V_zeF2J(n7N;Vs!QiHgM|+W2fLGY-PT)e{!XHbbh0_*}&(7XG_x@o0I{g&`~1Ag}r{=j-nAAEA;qv%v?d?3P)$!>3Xo%2>3j4w&0%O3N$ zTjO`C?%gQ%{fiOadI^4IOx;TCyl2*PTW<>Zd1AADc+TX7ysbVU`=={|d`CAisIoegvbt(BqpT!5n!tSXMXWTrOTvpfG?9#7ejnr8&Kib?snn=;` z*>%wQvNhBXpDZMq&u_T9+!yrG#E!*k1;NFkWXXOc6;}669;emau#W|NVTc^6WKIBt!9m zFXq!g&k^(+Y0HfD!})@FK219J@^7pB5$&U&$IzNj9=PZ6W zsx*TC>c?(>8?%jf*wFaoyD9iQHrCf(N4preJNWc1$lu`e&nwCTqx^E%`4e!yw{=B% z)Cq=ZQL4F#I*(Dj!5WO1Xm@E^q(OKE_5}QAcVH;r^{yf5z5LjswYSQsbfmWldO`fK zvrwgk+Z$H!;pgg5OD24ocidpj= z=5y@7kr12EhwQKD+!s78Bs1S8p$h5|&`;!BbdNx&>Bq@@M(YLr%{&r;_Np;(*LYsh zN8jjzqIipU-(;j3`sShr_eAXTyg}K<2fZX=6dn8@^w$zQO7HHnJXmeQaf0<7WMyMY z*lg*n<7)(Bh!?xDwLt+O2VHq>(`nfqKW;OZwaBHwyzi%ss?(nUFM)bX6IUowkg{Dp z-GlVe3w8gU&z?^po;~LhT6w(f8y@L%1_^{lU~eJ*X>Fn>uP{t9u5qkD^S$=MJWYM8)L^9b};W~!}7ETN{Wm>n}0e$ zy@z%AXUWiT-qrtt41q6bZUZ*zUPl-G)QmOCAL~k_3w{>ejZ2slJDpn+RqyXuOn`oY zdTFHtA)r-UsXj?k_{kSq_&A3|qW6vT+ju@&$NODc@uVWeQU@<_6GPUYGWcRr8gUKA?Q9cLtVM0S@JS_)<#^1_nV4L zY;Gou_dS@uhAqzD`4Re0BjZ01VoP16h?00kNYbO0(z1)xY;^YG2^M~`aL`--$be`HDcpz&oy zhMHno;|)8=huo%wHou5i7a7A#dUh=2KcYNDb1bEny%{Wa7~m0IEWZ!!#>O%^4>g1} zW6(b&z8Wy@u+(0^t4%c+_&}h)Utr1Mm{cx|Y{l-FNvfolOXihah~w|okXd^xtHW$ZzkoNv&`rk9pERT&eBE8PVzh&PYc{g@D z#`C(eh2S$-Trn9&n2cC{PZfNBEfxoEGj5poyWupv|Cb1#f=hQVJnzukT)DJsFoSqU z|K?hRU*XxEu71a(EZl=A*I^z{ou_-Rq`#AO8Sf(EtL9a2B2=RI4Ehu1%X7ZTC_P&> z2KsNK5U<6REpm<7Q;J3Poc^fu^u#ijf??%pcMJvc`BgVwM+&ARFU0*gw#pFk7s6yg z)oJC!hwtlKTiAe~4@$>;B5N^2KLqc-Id*8@nm3^+e-;OKGEyoaKCTPz*vip)lV$6$ z^ds6om`|otNXS%4*c9%M>_en`wc*3mKeqbZ3OfMv;rdxx!I3=1&8tuRbt=0g{naHB zd)kZ}_DA(zmhlwvBK6RQ>6>{X)Xy17<(p(1jRBrek|Oa>m-YGYEn~om; z6|CJg7|$pe^`n8l@r9C^-{xcKFdr??x>rT!Yq#>exRc<|hQvEV$HxZsr6?XO5z42d z;`Nj8!W84Mxwe1%af%~mVq^P%#xuqs-vWQd7YK%i7SvU&5%Crco@>5m;LZmzxU z{T7yfI;#;s1>>;bmsDv1x$AKj@eV$F@`G3;!gKo%qxghrs;PF4viQze{%w1NhcVr# zl}4SehMIQ4nvVxFo>I!>Apab{P}0Qll=Lf@NyiN4yGUJH8gW_@Pe+V*;5q;F#S$tM z5xy*j`ETtWhf>GgySI1ECt9quXMnz~SY)=mOc&jMRydw8%UqFVzvI*b-3^4FNP+J zmaBhkP>f7?LdD%FU4{6O(WWa#UmL>tWn$=C-?uj@@I1S0(0QxtR=nq|qhc8NCCpwD zZ-+5#IV<0n{fN^~MRcf4zxj!yE9|ZZNM2PoS$1mTIhzx+U?)5eo45e~~v5z#gLK&)m!5HeJ!03SD7Yf%wfy+SSJG2P?^(MMvh;O#kv4 zJSrL;U3^yN3IC@9n#ZBl)XM8mjhlUvTr0tYhMJuTIOPr>pai4OBR|3EWgqTwP4N9{ z@OUQo#bCwGQdD23Y#H(#tKvQ|F$FMLaz(FnBhY>VHD_%o*oZN}(6nc=H# zLxyA-<9dMi4aV=t)?0OUL?nvZL}5YwcykxSy$p1!WxNQJ=B-BJG<%Mb5hYp{e-NVw5F)6!*o#nO7ye6_$=FK70kQ7 zaKZNdv?1WXFkh`n;6Pon-9GyBriQ-=IY{qY@y+$T7pEsqBK(}yrR~k_{Jyx-9p+yw zzW$o3kLoGY(IYtSq4ewj!voN-dAr-30wCabbVIrjv{e z;CIX}tmj7gB&Aooe$1oa1b+eFU(oj38HT58d}u@UeC&DB(Py*`xVh!p$KgIq13tvT zJ1NNwXtJ-JTkf^X&GF?U)W2Td>~{|Edzkl$Ax`k*%vyD@(_rt0uo3t0&Jz*EVg4q; z4ik{CUoN92HRUk^*ABz|e)lD6P>>UG?skFP12NM9&NuiA-H@YsagAP)0qFcOi}Xvh zaRTQuxi~3w{v&82B7Lz%_URo9gU15&1u);(W4+Sckz-d?0#QFOcJo_qa$9Q#(X)1B zEE)L0Fdwkh=&SjLr;|8@PhowYL%-13Kd~bpW)UBO8Qx)}v|Q0?PiLjF-%Q5qB7;GH zi9L4WV&VLNUOmZ5yQH9*w?F=Pz7tP8T6{5$US-UXn(?y#_Tm*O`MQ_Lqt=C4(FR0%^|6dWc1hgdo&f=4x9=ENVe4g(}u-?Gp zyT?^Q{qM=ZLx0d06-M)u@u7YiaDO_sy8u6`_JxoE_8mpyg zwl-UFhxVp@V9!AR#XT^Hozh|`ci$n~(&x=A(ziRTsi{VsEq)iX|73;xLbv`rG3e{` z1Aa$?{s4{LJRYySMTP$GFy!BWhssi2LOt({t12_nVP56G{GuBfjqGI5#DMc)GiST0 zdX)M5+4}r^<34e}xOjULm%A;NR4;~pFelUx)(7#@y+$Wyx^wq`^H%zdY@V%uQUUt$ zf(S=rM(K>O^1T+$5bT#Av+O}xWs9%h=Jrj<|6ptXG~Unt8HUgMOa8Ho&)-L#7MjES zwz@z^M~HVof6$a3pCmuO{LL+fUB~U3oP77Pr+L8L1P#M3glE=emJJOLbuQkcIo*q% zx3*-6NPF5|9P!bF3-*#fP5I{jrf)|6~uRF zEm$3yf%w&C9nhb7e=|wAx>oMsj*wStCste#j9>UI)i~D)_5kL6#Hyz;RO?#S4=L|3 zg838N?M6t?YIlT*exooRmv{Q63&aai^3in`ix$%uR9%c@f1iDKDwF7iMQED4P?yGO z=M?kPxBgGxZGUpq)|YsQ|6m@Q)-!_hyc>Qe*|UZyzJH~!Lu=@NvAXn`et!Kw5U*#G z{3jk6x|Xn{P(BF#p!XH1nM{v}2ql==Su4K~M(6CJ znRKJ;x~o9HP-Hf7`0#sJ-zl;26eo`Ap~yd&91MCgz=tYxV$a*xcznPZ;W=;1^h!Hmz0Q-8lDb*!$}R0pkYSNx0LwQ)Gr8C8K(8=PvF6bGi!^M`C{Lr6<>O1$^MGEO5|C<-RUuBP6 zU455EkhSNEuaf-xN|X1+!Ns9gy{+-v1k)nnizo%Sk2;N_*BD#N``xUVk*++6eoZFa z2hp13)>Y*^L)4$-AG^Pq`|wJ*=?Q1Rqh`qaW}~9`7e{||ya}@0w7(JNOOmCYtNkwU zp4|-ocLJN zW*ApjMP6oX?E%Qou>4v-qHz^V>*y~Lk|2;BeqDN$TE7rG5VO<5XA5A$V0@EhV69a zxg!Rx@IFK29;;4AtGokk!^TQC_Y#5JL?lY|j|*xce&_JbfIoiY_I`2JKo#&MLEj|Y zsil$h$4=?@vw0|?~(Wj z@sqC^n1H@%;T-!gosxd_fBmN&^Y;{BPa%9vNLoO3qZPdxCzw4pR$UacT0=;#-IYv!t_nJ4c>jx4h6AoxqYzdqb|9*P$lR&j=Utp!*Bqd<#+jPB8!u7il zp8}sI(6dJ7VDj}z*Cfe(&z6y){{CYF59n6No?#@Zu=%-QGt#4wz4DiQy*kV}>uiZy^>WCS?5gL_= zjw=7|*Zpcb^c#1PfzJs18Ae8_*(|aLOy9jP6sntwQ}a^AC6lMQ9b|veG>|x;Z zT1dr+>!s2A3jh8rER!Gj`*{4M?W&n9Uhxmr>JiAN;gskhr*+lUv6!ao_7>g$;uAv?yV6j6DUNu_(IaxqTR4>c zlKN&2Jtm*PH}4&Gv1<6DD3}E-Ad~gmHpSU?{@4w~AF^&WRdL-_buA|w**9#qlDGJM z*R{;m%s{aB(@~mb)KVpnRGz%o2wHF9c|8J$w87$2@~#wP)$VH>-Kj7yZqxduu~Fck zIfzeqZwqy=?vJH)&Cq{>_?p~Dw!N=7*ezmpOpl~N^Tysv+(74n<%cRWxi zOdlUX_!VbTkifGr)}6AmcUG6^3r6~2cs!#w3CAsO{($+lTl(4-ybw+E>!(K|RG{AD zm{aDC{8yyWA^01vm@U`JAJvvx;tFFtXv&dUV`}W>ERuZdsbjulOiGXFSK=5Em{=Nbog4$@>zk{Vzvc8f4%CAU7o-vQv3~g1WNuZ8|MdFd9@R7 z@Idb*81xt<^*Ea%1)`Bly_M6}K(xQLKKeKtY=04M-Vqrz&xYs>`7*>i`0;}telszQ z2d60kz#obhJquZj^6RKv9)q#`V_=Q`Yds0RSMOz`X^Twk>hzm;?B8^YcN@)z!BvH&(f`0RI5JN_LffRhM_orkp)sADk&8 ztJ=ZqM^b5oVC4S~g~&$KM4*JU5Cp-tG9|!%De~fGF_}sONk)EP;9=)b|LX z=4kx{g`h`Q&>u;DYA@dy%~p6v8}UPWJm|y)O%q{Svynhu9?d&Dzvd7$biO`RcrC*LS;o z9-P4d9`y2`itbW0??ekX90mH>k3OGjQipkhb9NIO;*j1B%ZyIh>h;IIjPTy9M*c}g zcE)Sax4m%Tb9hsr`ntp-ciSxJCyG-Pg3ut?vo3Sbk@e>nmGeeuTNK7Kcp! z7A(7YYyrx*#9gUX9WM4;H9x8ULie}M6wMF2TsGiXdf5=+N7YJ?+FkFg9B)l~1Nr;3 zFw-fi=xvPBjgfWR|D=)P;K@{Dc1?LI$lA3e~oYr=G zuVEp?D_?hdhPu0ZavsU5E|Eh0bz&{ojkNZ}B~F8x_3HgI_wr`$iSs5NsrDL^Eht8G z+dD06&k^y=%)OiEl;M58)wO?)m=V8g-0wPhbjx6zU3PLdy5FLnt@>#@Y2#)9=}r2ScNN-<>WHca4og zd{N$o^$69=)NR3x|MNBs?LYHG8I8Wnwf>&#UVq!=#4_KBEdj9qPxVTb?GWCKoqG-X z??Je$o4bS2{S_I%#XM!)*ulmpM*;rv=Dyyd_k7S($hdW9MS3v%zj#Y_vtw&+9m?-n z2W_;+b3&`CGQRap08U(jTY|eLSwE8^f+JzU*5Kojj}RYDxWb=M zNcYm2Y^#q2`~7X_&K0<$M;FeX4S1xqrRz-ngEEMRz<$NXQKwP;!R&l0CCE8b<4u9! z6!NF6EE)f@Ik~H?f2?_k@GQa3k*>#&yVxOL7&ugq!R}Mip>4`Gv|qFD7n)xkNyMn- z-#dkyog6x6(^Pha^=zQGqUOB}NCtwSr9>*8Jx?>B_?Epv$?sjh2X zAxxXu+CZ)}ltmP`my%A|F&$nEHQyWmyIs=!Sfmydb!+sl51nxFp{` zZV&UKU+M>+?1l3Ke6Q*zYSpf$9U9(yWhMDp_Rry=0GMxfXk;10XFcTeBrBTkanHGV zT2>UpAl8Z-CXg@Anx=gkmgJ{c%_bg?A6pdYDHH=9$QLH@CaL+aES@bhlJ=9ze46Dj zl*|`j*p=ShegoAvu{p6;T$jwP(x<&^5PnE@i)*Y=_smN?$R@5bysAq)kX^+WOrM7? zb-=68eNF>@a@Xn+Cslp8@BCU^b(Iq4Tye@nVW zaDms`qt4me_|vmY{Y5Qzc4$OZ`tgbgXg>MI52s8iU5Cwk%Zq*s1Z48zDrOqsbE8;4 z`K^n+jQ`8WIzM;l#2cX_qPhoJdo z<6@|x-Zk`c4byyq_fhbp8HZODai=|vIiK4%OzlVCZ+jI37_vY=v7YTYTedt{<4^M0&HE-%g-fxlv z>JKy3tajl+ygSKZbS)3GaU&Aj(fb!%Fj|SP#h8<0yEt^VM;`ydL!Yk(W;qxJE_Ck1p&u4)NWPI1KVxu|jIgqSSF%FSc0z z(=+XDs|XKQz71>v|GX3WaY|hk^XJ=-sjr$T)P?zGzbTIh0f2WYvBg~Xvh7@_ zvsL^p>DQY7n=jyzZb>^`gYX5csb)#rq24{x;k}jlJ3foO#d#hdD%E%k9YCKF{4d#q zNLyTomr60{+$*X7l~lugR?A=gQw&Kw6yfK1sKfQUcUrH;0+jDF3r`U?>9Dovo}(DB zM?EZmMMe4}@4ZgN=^DUKw03hUa)$Z{)4E>?@a5tz@|6PhjWpczYbO!^`_XHbhYf){ z5tLjLG86{uHNyh^E*f3`nejj-px7MYE#V`-hKs(?k#Ou=co{`Y^ z3q6S3?WucS1=kN{A$yHIL2bHVa#Xp)Z%yU7?SWQqK{u;JLQ$P8yY(F9aa?_YUP&^- zAI77m6)b(;k`Vs5OG;|$m6#3o1IssiLc}r{U)yTrve0~;ezA`|alEOev}MEeGD{!A zIrRHyeutNyKUC~7GZ<%+XQ;Xk>WQMbeA?B+$M;nZ7Us;o71}zPO2=RkUkppD##ryW zxGT^2L}mK5m;d=6r^MXzCqg9k5Q?HqGwIIq5}7-|e};K>;TSxJBb}DerUmvK+k}nu za%VqZBUjnYT%7Rx_MZjHJ(!=O@Hw$@gCoMrL^kT1Xgs$9jb&#}OX8(m#g^XaiwfN* zqo}5b7oPG3`#UGTe^(>oGvq&@*XJu!WlTI_8@FQ0670)VRH7!GvHXG@Ikz@Nf*-=X zQB3I`ea&T+z?TMnX`X9FS{lR~YxaRZfcWZ)%%k?h&VRIar9A{ZV2B)+MlIFnFj`fw z2BLbNIh8J7YTRF>EtMsi4*>mJ@`rd`iPeW1H=_5g^`z&AdaX31e|~x3=KN4Auhy5& zDE=0fQvlzA{!TZTzojQ_i3<%7I)oy;$(2A$rYrN3jYtlE!rx;GtMrXp{_bD9GZ^~O z`&oFogeT1XXT%zyuMF`*t;fc>L%hDSzzieHt!svkZXR|w8=Z(ydNk53HTW?Ow@yLD zg*PFJYO6_#%YX6-@VPbdm(^+PZ(@IG#NT3OPQ{kCw`C zu0Z$Mwov=g)_Hqgs%SeQy$G`F_s0CvyY2QPva=8`w-)})se*huDo#h|EUfR>NS({n z66*H^UjDj1hheBUEokD3MBOS=G%vXSU~jT9MpYMJ-fAp0p=A=jkFp?j`O{wu?{?i8 zJNNKN<-Kysnm*v5Ntb93A^eqP;!2;Jx2JsUk37D;k9N}f;yCfBut6%jLb(^!HzRA^ zy*}hq+wmtp%buIbh5Isu>H!zn+z~HHe{eH)o~@0q3;3Gu zQ2~02Eo%kN{r~oxWHD|imCCG5*|^UD;-h|SR{_=O*_zarH|I__uJB?X-T9+C9q5n2-1g;>&TB!6WRZiad|gh>tq@x`relBb98m!ozdR&4c@H z>7J{Cem1d3ZtDS?@bU(+ViwsM~w~QBqct693dR-N6~2KdWQ+{p8@B zJISW8h|gwzqJUaD)mf>1XH3#hWnN34=-skZjuBLR0Prxtk1+&p=*o6~QO5yGU(D)> zcVe-N%z_3fP!3aZUm4~$69^J!o1Es`f0T;ZeLKx`=3hMF&t2iwQp{cI@#nTv+PuE?s zA9l&QYv@C;AIYbD2D~=h>MgsKd%c7zPJsT${GaY^pF|Ad$Csvl{F zvXtQc8pW5i3e#?XWih&TR7>=fZl^#f5f-z48} zm2T(`ZhKa{Xj&iykS_}P?7EO}jeoc!C)1+dks#p0=&FNc~N9}NSmdv{r5}5L3Gc$2EclB38ej|#Oq0V8) zk{Q6y=d^g38o$sn{`)x6r(*4WyD=7%XGpw78>v9?M$!7^n8x%w?vY%(d;_EheoON$ zfq+}->JRbwqi%AN7w!eE?p7W5%~1*7UHldj%Dya3Ldp!vpP3%+&H?}2X^{TX69VzV zDAr@Ujy6{zJ(WcWMtU)|{y|>E?W*^6O19!rKW-h!vibiLu<(3K&JDm*r|K}G!NL6a z`hlgp?g2hLL)QAvEv0(WyZ=&vcmU4JtDl&QR^DdgYhFr2y&3rLC2@?(ziR%}bzGSM zeAGJzzRQ1&&smu3r_77Foow(#4o$QWlkdlUu5Wn1RRv*%-o;J7-;kRYEZP&X!*N+> zJAVTDi+{P6cFp7x3eu+cBYizvHW}~{-~(puZC0AUUoHQ$7ua{ubLwd}NGFqhEKD@hyXH;CF>ZY7&aSjD;XR)}|p3mE~#=8n1*);)|?V!O%ZfoHV!p zO`r@WJ^5<*FP_~x!CyX>K?M9DoVaMw?hUNanp3tjxv!~ZsJ{W`=ax)_g1zpJ(xDIE zr*-GlIhoNBz7mzC{{q+g6&YZ^_Nuk|BO4yr_CHR#gqIqpW-!t0 zDY@g9cwKTIs-xK$y_G~A=GJ8mxsR^Ahs`=O8~h=>ypSyE4`Z{T-$RRSXRX3d&-kC- z$8H%7C&mxR2Oz&Ys-uYcTD8o}BvF#jxJ$>yQ})xVwT61^@@YWm=-jt2 z@M_macv_Tz=j?1%egt3KZog=Gb{l525%`OOsCj-DE=c&X9-?xVewk&~kA8-O0>u00 zx8n!~*A8TBcBi8F$6V_>Z+o?p`M}c0a_0sMs=imSK~F*ueUBaFjLyGs=gw9Yjx?#y*qr$xt!e>%6WtO9(J8Mb59s{F^F;Qw}# z+Sua}Zk9f?OYiTb-r1Q8^OIzj3Jh6|J> z{NzMOo0l&;JNo{S1%CwfJxVCA%A%lOKljG`Rg16lic4?aK=ZGtdfW@}yr&LMGmWLK zh9_!EOX&%NY0oI9R8OLLz4?@h&@QO2yE}zVB0lB!#|&ez_g^E;?ojgc=Q9WKWq(7x zVU!h*fg6*kMS8z?)^9~1=+S1O{ES&tFi=;4@Dr1cd5O!c9?y+x;o$zUS*ESS%~gLD zJ6P`;7`xsIdefW7MdDAM*;@PTQG8++UP*b*&&;{KyQCD&C%|TPJT;y-Wwp?LsV@4N zOzilOLMhm`jFTW}f%uzs^FwLA+Wo6@LtY&*mS>keSLuZ_DHgkaFj)ZWH7MSD%IvCj zxrsvG6R1Z(d`@h0a^|$|z#Ta21NblG?*;crI=h$B?+XNmbR;cGn3MB7*PG1q19!G+$YMHL;+Fn<{P4cg{Wy$Flb z*5-2U&Sqb~F(2ZiL2;K?SI>K;marP&ubl;Zpy@Q+T)k#p3ts;o=;@5I_UTh~`5U1= zpE^#_BDNlF1O2{lg4#DN`4_fkO$V)Bh5BD6;?^)m8W5fs<)(t6=Jy-g(?0YAK0YeW zrdIYGwe~Dku2&zs;W#b6Isx+Te_ySS6hZ%6Ze=OmAMB?v+A1K#3-liN|MDADGTQ5> z?IHdJJ@L>0>x+4DCm$Sm0`-ScvJalCL@naD70O=g`r+TOPJikJ>gQcw=BfkxJtN+$ zPsN0)GE?8%|B~cOz6INs>t5C^-7Oai_P6reH#bt*;`q9ZRyPH>57+pwkEP*ytE2{U zJv`uk_OLprPDFl-d~;iUrXk?}BD`q!`#tT%4iDBitXB^?UcH2kOJrONMD=pWpU)TO z-=hpE)aa7zCHmb-L=HD)eX*WQk_eqIW`y<`w`vDg*70J9Pa*yy1@=Da>Z(<`ZAeG) zaHMO|cD9-;=^H2#9@rMYZTx193Ge8-8qHoy3Mu8 zy-(QLEe^20ZUx%(=zwR@KWdF-`m;;Ep37mCi(^)gij|ij{Eumxx;2{k$^2IzLl5N- zJI~wE)C}@kH~~j)r&$ z>|Yr*uHnNyFWUA1ls}UF)MYvh3lw|u-?bxuPc|u%vVCgIRav`|e{Ol970poY(0AcE zmcQQ_4>Vt#6&?o{AYe_$kyJjapGW4V%9k|uGg5owejq%Ig>NS6;fQK#e+g?)eOR~? z2b2oJt}hDu58!!+$kqCQ$9b&dyQgMCybgT8(2yXj?yS^%R|RmsddS62zba!6AE^9k z33!RylsUDzzUnA@OGei3Yclr|t=4Rbs~a7@I!~wmvyLRbaVyXzMs7^ZY0c?I^#g1z zT_%6RYpb-g$1SkGQ<1mm)tQ;@dDUxI9*6xLB>M$g>(Bow&ym%~`Jj{eYw^ zqthk76nS$BKJ7P9KQ1{tj=I(JkK~8T0v=MN?l4!i)F#7x&s|5ZYc5*Vb%9e5u;cF$ zv3M??+kOG;)f6-7<>x_%w;?zScb1%HAeS zxRl>;-bcj;Nd#+{PkAzP%0_aZ_9;p+t%Yi zp}3mN)mwER`4@2k^^G^W-=Zvx;{!{5`&cv1V4@-xhpnQtFPpPW? zPzkMiZA2>gyKWZjCyl+~`PRTrcpu1*Twm?kSDvn>Eu#*FsEj9{w*~-zV>%i)Ope{M zCvX=rbQScki)z1Dl`V$(+JE2E$H6@C@JI@tuYmH=h%kFzzp~A(H`nS@Ee|qoswdJ=9Zo{vE zy-H9I=*KdeTG`E3M!{OOV&Dgl3U9>0e9H~pS<#Vj9zlOC(cH9BeS--}VKQxo636)X z=l8e|;ICjR>(%s!?*H*&-rtzN#=@gSCm&S#Xdx;6c2AHz=JR|c|Q5N~p}rSxqfJ+rur;?JF= z?wKR=R6B8-8&qL^En3vgO zwD_-V%|m1Q=)9Q~M;mEs-%t>BKWT>Z!e{O&Q>Qa-ngt#B>T9bJ%Jp{&+dn6qe%j8U zAA7t#igB0=`S$1>tNUTddt~1tP4zJ6-E5Q}s=LWM1cSZ96t**}ELCQ8Ywa9Pfq%qE zZ!ul%dgl6?TW1iTI>LS2O^0@BVxzM<*mDEmk9YsIDOoM&j>73zF{#JxUV*+i{Kv96 zntfv_40qq4oeTM=%w2$=P14V_{>R6iJLrFX_eP|rAL*Z(OcVE4=}8y#N$U3`-Wwz1 zK8Eyfn7<7BEAYQ;8%11D6!c?E4!u7!$!=oY5m(td@Eq(p-1jg%TZcAflW6(i8nREU z#LNd;HCr<72JbEO)z!kMchuo-Q9vd{pzKe-Fg7T)qIefYZ~1 zPY@a5V7?f6f5PZ=w|;WpS}+!J~ZFR2KEEyO_E+{mwYtif9cCuG+)B!L-?>o;BM+|iM=%0 z+Q^|RG^R%PX2J6S-c`o2%Wl__lQ9*c^D6QenU~M~poo@D>XXOmx@0q#Hcv_=dz1@JJtAr2c-#qS2k2emRG&&&R-)5?F2-{^o z2Y$6O=+gCi(&c1408^`2ex6uUSg+JB@n}Q=rQ^e~2M-3|`?{kdL9Ba2MdF<$x4^y; zriJH@{+X?R@X@wI6{!+FL2&`bTPN++k=uKj`8yFl7x|g*T(ZT@@(T;aJ9QrPS<}H} z`qMbQ_Yj{5#J*+kU%ZZtw>MMmoaj7&OVFs z_kivvh7j*`i!)t+1#_(*(@#d$AiTcrL|O`?<7)@$>sTAC_aw9EZNS9x8%8$x_(=yB z(WqHrn4O+Z5mVrtqz31Irq0zB_)GI*QYNE>&_AGx`nROcNbw7b&#MC;?wxs7^icE4 z2GfA*YP$ZhVt3M@5Bc42UfBl*Ygc;v|MVSJb>A{T`gq_UFxng#xI=qvr6AFZ*y&HF zN)==p%6VxBy5`?`bvsQJeADdg2bDw0Xni7mnqIh(6}C!k*m{aG^Owc_%HkWdf;nu3 z#XyAQeipvvI91-zh>hu}m(;(*w6uw$Jp0RQU->}%(J#)@-)?7e>S5UDD~=AJ&l{Pm zy;Z67vtHVOkreP->I?Oe9@TWz@VF=vOLhxybXA)@lWm6s+{C*YTeXh&Cu4Ptvt|M z7cr0AIP)9w<8GGwhorUj{WMnydamAQE=-^QU09Cd7qT(6C79#RYz-XgRnKe8Ia-nfDc-!r zy^JUeN&ewM-*Ue5McSKxP=4(^BeadXH`*6G6eg#5kf zjIb|Z2cs$?8_vfND@?!2DMVGdb!lBIy5Hsj^x@%8b&0nsazYQT^s4uDLV6;?daE6$ zJyCuyj<2j}YwKPvbX80oXU-> zbDzGO0JR{ixn1dc`v3L!t?y<`VU^Ok|Kqo-WTb2g510A+%JKNVQxG3FS*>o^AWm<^ z{gws?xe015EDy8*N+va5ijkViNk;MjFkWGuq{<2pI z`ERo;&+*mzd)EFPyY@TqEv$*oU!Y$L%0RzN^joNKKO(eixm<+Zmx3x|Hbmq1HyLk$GlTsX)wR_9odXX=sf#gMe5EV z=*biJd2_eBzK~F zhj=?c8tj(kaVD!b>*jJVYogPC^9$=yKP%{8?(=7ZG#EpPAq;i^dnUN-OH0Ni=?pqO z)L z4+!_9!Sghc{oR8|oBp9(JG9|sqjW&ir-$)@KS!hMFV#AQqkf=BkBvNrk=#xg5d{8b zTQ4?VYo$ut+#S8^D#smuiu%n1Vg(yMP6wQlx4Z>aH#%7rfxi7Qr*^dX|vynbZ49Y@YUU^bwRI#Q9Oo?$LP@LxJuGV&=cmt zdBf>BB;>zTuKd1t3jFV+&=02*aP-a+Qd1a4(*IMky@bPc^zPL+(MEU^%eSq~sQhq; z)yjcAfG<^iDZkQP)L^B3o@9qJ1bqiZU) z!mZ2@`D6UZ@bHjg>BrsrZ=fG`R2()nF+QHC5vU>P0DY<#Mdjzfp9rHVq!4rDKWg3e zN-)2ql)KpVsmLFaU+O#aEWTpv12loZ0sY;!-BqP=_*>N^jr*QHWp8P8xo*~GX| zB$%@AZ(9KKYWlHzpSQEFGAfm)g6V*VfV{9e4L+~(e0ORT%1@ZNcH>0i>m9*Ar%vU+ zfqutwlf+jSWAlr6)YWjmn#7m&acsT*#)n}FVU*2*FMOShOOgJ@nd}~8hpphRBDU-5 z>(>ug_7=@Ig!_oa^PTA$0f}7=^vSNqg3^IMo4$iSl4L%zvG2mL6BMH8=dktC-{K8Y z$X;U2Hq!def@ssG?R{vyBC7O>CZ#G{j(VDeIKcC>OzG;?@BgX^+~DAh&i77_RGBj$ zRxBFq3Bya`lf4)mE#5Ult!;*Z>eG=VJ^Uo~wyfIwuwI0>Ff)BQ9NOW4UT)eE%Uj$0 zcwoDl=LrmNACWzFQ^m{ikR$O6|C``(oK*4Yo6CJHJRA!zei#2uqvWz%794y$|4udH z4~&4I9pCx(3fPYU^Xb^w4bitBNZp!b!sqv3!*{cFhBka-W?b^a41Dz(xJ-;5ot`Ik zLGT&s2Q=vChOIZ4_dM%lb1&S-E>>=;5z!6iZLZm&1^0cBd~Tx-hrqsgi2diEC_W)a z(D8cgZC65OJM{f#a;>uI&>s%{dUvOdmms{3T$pC$%`5xU;(kpls_#W=8N0aExH`yX z|I$P8B~t}6ImykW4mPbVMV~LuqEe$RRvA>^s>WFQ?3c`oE-%#ME&QMf@m5VE)`J*u z?7nLD8ENL2#9tR{5y(;Z<9WFy=3o!S=J8J$Wohvmf$E%DWN%rony(?kwP8P@{}QL|9AX_- z6WtvqnO7YDcKAy(=y`@Rd*SnY#2zn?9czuLO8s(h1p3F{iFeZD)Pla%c>h)-L4L3Z z)z7l>e*5OhlIBVB)2!XG$@sk+9=N9;MfS}sAQk_iYJdFXqsX*|2Rf>#-+bZkth|QX zFHWNPmV8B@S~bZ|OG(nY4ELFhH6xC5OLtvy!s&F~Gzv9$cYYm$^j$sI%fkJ@LjU_O zx!gfH)j*G?FR)%PAFM!IXIoxHXth^A^1qRhd>C-F^@@SvvS7;m`_FDFl5~(>UvQ=G zvWAUx3gU~N`2G7f$#>p1@c+=SL)9VZ;3|lq7m3ziTxH*^pS3>QLXduWjLMiH&yhvZ zhgaPh|I6;YA@Fs`?uAZXqCf}vZ+~rt{RjQ5GA9zjrsIfy8`Kv$Q+58LVQN8E{j;@= zMZ%>;XNdH%=X=L}x0LxZdmY7tk=lXPy3~vc&r=7$e*(WEt1Q2O zk!xPV3)@KPyF~9*I@k;6y{>tgwcA(<1E~EMfm!**qT|QQ)spxrzOStyPX9`#tvT2W zsK<45aJflZq+E+2b@+LZmH6Sju(3=p@Md23jy>)S*K1#Cpr7>g$M>-vC_l!^7R=Kj zZ<^QhX~P_f516La_+RZxtg%1jBzms3uYu1R?bfg$uMLmliJktbDKY17WhBHt{c`iy z$*SW{HUU%9{JGSl(|!#n-bwn+Hy#vur7k!K{<@}{b+DV7a;Z(e+7oMVP9wDJIM)a8 zA7QJ}4OCY3VdD%*^ZRJRn;<{~sED%mSpqRkR9 zV~`jzG1RS$N!GE3wp%3Gnz2md^E>ak-(Uar!pvtm@3TDTInU8G;*>9Not5;{*~dt` zv;^yjog8oQC+(lb3a~5gL;7Nk*q_jU27E}LyQS<9cV))!=1r(y7M`z4pD!7pi0(#d zJC=g~^$5TzDh|k>)M*0$2l@Z*MxH4_XYXLIj6{!BRH))O*eJsu|MS@u;4chvuEP9C ztLg@afbevLhXEgt+;8b|Pw-t&4)sf*f5qWM_>?VTdSZa*1$9<3GK%fIj7Czkj-vXL z5a*!7Yu{xzi2n-pINkNKclb7;eMU6$%c5zIM++tV!En<Zc9WI`F=SIg~?TbB>X$#LqKj68{wK z=R1LuHgVP#kf5K`%-JYr6i7M8cOG6ZP>{P-y%ATa)F~3vxGpY%u;0nSy@L#T0OFc& z{U*xW*PESQ-$=H=rYDw&N{`4Qdkl|$mCc}22=5omE7aY7vaJUA3?*}qXfGGi$>Y$! z=gd!u-58T!w5Ap3urKk71BqAAL^Gr40H>gR~4I4eF*R zR>=x#4;q$IiEi!7Bzg&c{T(JS9}Ri|#`Km8N`(opW_%_2=?R&5T+nieM|+NdJx+^F zC&$~i>?~cdV@3#gVl(GuaJypbp^>e(c1a852N!# z@PzP9Y~7gXxu5v-b3#R$TAV-36U=U$dZ2~yS@SSozIJ>}|C#CC6vPLpf&HRNk-CI{ z9ntW9wsENzt-ij0i8YQq_o#sLJjB5_+E*O>kkI1}6d!nAoNfCq>yOA0P9oz&(wZ6 zJVLV?SM4(aeNxa9IB75G2k|iq;15G$bJ#cjpr@+ivhmBstIcvA%-qB{qOGg9{y~HO z6yTF_D%JBpN}0leAF*g2Pu##XG&B}gCB|HVeiioH%(QI8QK^{^ra={-?S}3*)5k9S z33_G`3AwpiF|Nm12isUtg6WxO1upH7zw8x=OohpG>zzri$B%)(X7oTm{L@vXlloQd z`{Ep-exY3pp4wvYW{WfDr^=18|DQ)s&Tu(>SO?`31j^1s6mnm2VVv11ly8_vm)Y4f zr&d#=jJW9i*PMuoiZfnSsqNt?EC4pHtMWG{P)dZUo{zk{UO}c}kp` zl9~(nn{A%)$_n7$U5slKP~p5Z5-jp|`TKhx)}4NOQ&NBK@oS$7yb1N4?;zNlkeY9t ztEKs+5?}7T3)wg5?H4pOavKxR2+!cXW@6vZJjNb~I02V+=9l90&65sgKBjWUlrDXz zZIgCPgZNkO?9$H?_65i$YfJizcFc|To8ot3$@txxxXs#l6e}~2Lts$%_G65A($8GqAjoP@bo0^P8y|l!Toe& zuR714I<@bWwD9O1Nk73>zMxH@rqbre>3ymnUen#|{w3!Tb8k+1to#@9=0z z!!Qi#r9uDe;`XAu{h`G_qfmZMi1u>{!WLbha9>-E>;uycW?Z_Ixi&L<(Ey59BAr)S zeD5aff0dpc20qIm=T%OQA+}*P>%fDXxMM4PzEpb64}2w@ve^4^0rCe{q5O9q#A%0H z3qDFq{O485`F5Ar`=X2{zYk|{Y;JmeBgFo?}IVQsU*{;bTP5zm+gVqy_-pA~LRu+pjBTD9_ z9fSLU^MyZuzWs93OQA6Q>HFlTDM^L)Xy1j?#Iu)O2FApb?%vr}I`cUL(pq_1r=_y^i+ma90St@wMN zT_o6R55cTzyIDcp!-`4N?*#sJ*J;1T8($tI*eI4u_=`TOMXZCqDnduZ&jP#w`dhj2 z-6`d9)<@7hhbUTxUQS8eBA0qrANY8V3?r_R5DTR)p!WyW9p$~A1pS~tUe!H#)`i2-x3{coYAp%?doy!!>@q?3 zV^P}MSFjZ3;oJW~`!H*?#1%3MLuvdx-`jupgFeJje)(OxrcXe2bP#7y6#P&8rmiL6 zFNa|sh+#62nJsWPB0~NE`@M}cbt=~^Bqjv=p?GVNp;G=1XScF+C+uT#0e=|F@1UO~ z@2gkD=}Y)&dsGj)RpzV}Y9>BL^9h6ViR2eadT-B$a9UP#4Q3?88cqKe#5cF26lUmD}pzkqA(8aj~3EXQ0$D4bz8i0L1R^s5dWLc3g zmAn${Z<<}1lP$|wy#61OAM_($PuIYD*NECQXC3vwZPD{0_ykkSasPUSpIN#azJH9S zx8)(p%XfY4hr#)n**M@|7km;xJbaN#f&M6=hmb~bX(!s-oxB!&dUVJlpF%NtAJE6NKP~C^J{3aq%vood+a!|+ZL{Oh!$$wH04!Q88{}~xA-tOaj@YkoCX8;Ho3*R z=y{mO7H&E8^ZC8+t7{LV_s6-WDnsvlWZtH!HaGqR>V@mFS^irGOmym#Y7dxRt2@@A z^GN`D7FidWG|=mk;3*Z3s`=}=Mb=}) zhk70Q!-ctg8QG)uzxP2hC0NA%#E#LDC45!NtJEUbsQdpoaud6z!OZ3TlaGj`f#iyX>Vv9RBz zEa)+xD9$o4xEWMQOo98`i}c7I!hR&$tv8FMfWO_8Yp96y5U4|iiig&%=8{)0I#e?@ zH9PY9`t{YQ|A1|jE||k?gzUbh5P$U=2zoo08TrfBr5*PIe1P$l)*~j?zqo0IK@tq{ zYKZfiGGxbsD2k<;JNn6t{?b6wrx*xOK# zW8*pknU^vlI+FLJ>o?L==)(N&Y+R4<9aZPmQb>Y?VTS!l5TbZT0eB!&O zRe>vplDN62dZ4NigT6`I-;PV6k|K(SM4UZ4j}Q0tLB2kQRT*9H5Wap_I1t>V@a!bs zd*WpH$0>}^*WDwv8J?$A?`R<3aGgUqjlUSq%e0lx50j*a^6SR4m&?y|=fBJk=za48 z6I{&3wR=hOAy%=8w=!w5TxR(;upfbid27eJ>L2@0-zr})Zj777<{99xY*;t{@)y}5 z{Z1pgKk7$jk$6;eKH0ua4RqFTYPZhYk|AFo<770Qd*UjJI$pmb0p;H`%DzByQKl7( zAg$q80r^AfX{M>c7XovUF4%YbG&L+yJ7MW~Ym06`|h_`wKeB>khj9_K)tcC*bLzkEjd#F|VcE4Dg5i&w?xVC@i4QS-~(% zI^<|A6on1@d5o-xZIS6&|6x9)MWf-U58 zy@Xf!4~=WSKO8MhE?v2`b*KIZ$@6?+12VPcT+kPA*`gZ# zPkhZ&Z@|w86GS-W(6Mg45{LY_x8_!ps-+gnicx>v&7$#MYu z1HMUdspAOGCuaT7iQmpV2flAt;2<6({NUfo-|p4j27Q(ZY;Nv-!MSzte!6KwO?FL1 zeE};t9OdUj1lT{or=?p*x|A+J@x&@!-qqXX{`*BNpJ}N7rm5zmW7gIh@%J(-100j# zB>1St?eL8+$IrjFb!37)Md#DoCVQj#^@bCU@ch|vg}9A)waHWZd zl+3ubIQ-IY@n`e&R-*gI^tvik5P!akLSM4Ir<>HKowo)7^G69{ST|@c$F}8Xz4Ogv)QyME48xzE_J3749UoS&srA zQ1IV8fdOMHzyG3$oJM{&2WJEeEGhj{N6y&Plx$hTs-I>MCjDFt4sR39zD*WZ&7MX<0iuU5U^vp1Dz%} z_TNf@ea3LTvsXHsuHrJUPfYlzBK`;Ht6r)jFD#YnQ?zK<9bn_>s*dnVcH2M^#4|=W zXD^*wNLJek{u4v-&w|2g`=nK|FyiJmF4PaxR0+nC_QxKM~!h z7mQ(|ClP0+f2>D%3&%rR+U9Iv?5#h{(=Ii{yY4hcJ3oP+kyU>f{QtD*OK_W^)}X?T zZjKP#RKBu{rW`$EFxn5(1CSz z>v$6V9-m*Y9@LrNLi#coM|bDQdFPhfsRq+`AU+OpqF=$i>uF|g;4}gL0rM-WhGiG} z6#{sFga3eiY94u)W$o`hG>W@2gYZYtdlHGl?&nBJ3oDU5i1ei=$&8_fbi!-!r@I7D zKWG>*M!iov%V@*>^>9eUKt5fV68HYDK;h|G+x3ZV9it-5dNQ=%a`AfeWN!t;M^(G9 z0)Gn5Kky$(MtLs%x+|Jutj6V6n&qrs5hvz|o*db_jmCoWFPQaM=3&UpV#baBeH!r2 zF)X&pPAmQU)t>loI7xllGfZ^I^D|(+3VUfblT-G6^DxW!3(V{wXBl-XJR1cMA-Q;k_i&!(*12t%%#jheKYpsLp>%sUr zj6VhM7qqQ>5Q_NK7NJhn)eQ$K{cfTCd%%~taE_-GpOx`FmJav_%%29o)i0FNEs5<` zNJRZmLO8S7q)~o0R@@na?1P<|m~&RVDDJyf&l#8}9K#Cx+grKouWju8b_ex?#IdtO zbJq87?4EZ>me>331{7WWXdW;Hp72!DFNr?8Vs~1ynEw>qLN&f#d67V)J0^h;E1Mw3GPe+4;_Y?Pu z@cZHXLBFT+tt{h>GmK)bn}GZX&g%xEy_feB2lC%Gs2@Sd=x5TSmR_G&ajzA{19p(A zj7;%A2Tr}7ISF{bpe8@KZG)!0)TyCCB0P_;qK8o*Xc4D(>8}52gU%P07TxGdRl45s zTgEv_zMnZ-IAriE_D(@;zXXpyX`1rR+BqvGLj-&Q$j>V&c3c;1@4Wr(OX&02?oIxo z=aw_heflYF_CDg3Zqqd2^I}@~{aPQ$x4&T*`pdGS*6;>$jdno(^RLyc^rf#f*Qgzf z*aMRL*WJ_ZK=q361OZvJk9rI7N4{J z0laJ4Vu1PcoyX@nUw=vc7QHjqhMHP z_1s?IC!l>tqC&3cHq)DrSYBSEP~UvTeDd}6^>>w>D= zY1qhUSfs0u@Jw-ts$!AHwdV?5GJYG@7G^(Klp`(~Q9E^K8_gQwdseGQXXlHbuj3x< z)Va+~-g4Wp*Y$@eoVsshWKCXRkDEem~$4FsKfc3l9ZU2Zdtk!>P z2h=}+$2@w=&^J)pBurTmgX(W~M|2UnqVpsX?2CS9DCo|{V7Mvit398TyUjLVFFx6( z7e5F5Ag2A;0jN)hFgsuWU9#yozFp0Czy}?;7?12HXUb6B`0z90hjQE~Jl?~7cp@iP zuH^>gPpr4`GW64>LEUkq$RAsU6*>4E#KpU}rr#eALVAK)Hp3qG%d&ZQpP~GOmM5%_r?1m>zO$`O z5|8tPNp>U!&f~tP6_R*yEr(7L_}=UBy)M2H#Qonq#O~ES!(Nczz7SZ1H0m?x_c?;N zXbtpvoLC3j>q~#*CLB7sSJJO+G-@JmUFYk!E!YR$FEy>v#>G1|@1aa_1Ik~_gLQfH z%;58ylu3Qa?*$^t+c!a`&NmiF?Gz&X#loZ2d9Wi>#&>>Hq7N?ibztUd*?g&#@QE1R zw6uNa+LM=Z(@f9Bqkb-dB46Y&yE^`{N_a8goh*8QuRYqQL2M6#{t5JZLi@w~wL&cV zGS+?v{dZ2zCihd|FJRv=%>TmmS>y{%uh2=cGk$%^s9vecH|F{d|L{KDw-NG{CtS=u zfSG9Ms5F$@egCA~abvf0PY=$EE<7tJJ1CW9iTM4?BN|)A8(m#(iebMQMk@>jynost zV26}cfi=1HQ!Bp%>EFH0&JK-Pn*sfDw>4j;L2t268I#O27jP%avaBm_wy4a4ej7&H z3(NS(PU(-ec;68J0`sA~JLL9ng#rGjCR|T9lme3^V&d45%8#s4RL{hEk zN$6hSwb!#n_!I4uur?Vm8Gi!)|G};aOJ#M|&&NGUj)9Qh;@@iJ<}_r+^bPf){tG8) zAKyAb_=xstPYk+$wvR5up1k8D?g#7hM$qd#*5)JOV-QCpJQTX)-sU7u!@lWX7c*}D zzRD5sH!PLJbJ_aUtn4RG!w&f7e|7Lv5FTy(e%=rGz0FvdbW;fV_)C4=zmKAR4e;eb zNhNz5>r-Kv1RrV;5U(bbs!a5rin~;w>u5dm-#jypJK`s=YZtL$Z$xeL*!;hP*u*aA z-!yZg>GY22qpMj*)WXKg5FYTN-DJT6<#%Dzs6RJ`Q55OlT#j%i{F|%c`-TYB*+mqp zrs=-XpXX8jWwpv_c0yovE%?U5Q{ZnKX>rWmf!-&GYLxN{=m!iENQt;61GmY+HOn=C z|27sLHmV`@{vkEw?t*d0Gf;2ZuKI21Z}0l6m9ike0ldkt$9dNajDR(m_@a7M{8Xj9 zwoP&IJlLxwe~-zY=51E5x!bldP$qjeJN;>7&7kPA!xp>i(=w?#!FTL}YcNsN`ZL_? zkUzt`V0Sj%b({UB4pK%M#Is?}HM*vjL0Zds%_!YeT%+#PFH+d;u=SAh z2?OESuiBB{GvRzf{!EE3>X=MWo{!x>3HRT_iB>go#k|u;e~W(u`O6ITC5f4qwsiaV zF-|4w*IT6d?9#g2zWC&7qENyIjFxdc>lB$Evw_Oa+6Q>gYhG;eE_)ZZ43CAVzNE%7 z+wpj*^!pKg0qFf<+x+rslY0}Y?8A!?UyxSi;7S#wH3&T|LN&bxz*4ykqP6WJURh5wTU6U2zn;e$el9?NYw7@^L+UDTZU%#Z|fhoRTphEWZ2v^zTLqe6Ngt5-Z$dBq)pOmPz&Btu%aT7Ne^}o^iXcR4#LH?G2S1kJ5?MThU zQ8r&v|MrNTal=|_KmDfUflvHc_%)=O+iuEGU$uJGIJ0^ND=|=1RXyZ-{Ce;yNq?JC zEGh_Efd=5Q7_HB1|=|K>eX`2}Ye$$vJPTJ`o={-_4vxCN81;j6)}Lc(ugdVhhWxT~Bh;2D^_SdkM{Z zo@G$+h2ylg{3MD8RToT)P1I{$ePfcfk-b~&c@@tjb=5}RDP4r-vDp>H^j-XeYbkes z9#LqBsxBz{Z$I?8$zKC4z*pvMdroG^c&POrzSq;zg7!l6l@cMI69 z3;QxQqy5szUasL7mZ%^}jKE<2=|T;k(9?iM)31fx(Waa&}6`cMc~8Et#D7 z#ku+2dIhTAX&GdlP4{jzz40D80rl35)t=&Owbls~ofG4_W(wC|)6bYm_#1~cdrm@r z^p(9`MOBf0;ZfM?ZQu{wro+>N&(oy_H6DL#mdp!^3Y{txb7%s!5a$wvr-@<%+>IDA ztGA9FJq`Z2nXos1{=$}HY^wLfCh%9?grF_XRPo9Od=K+7l<(8*i9pDhP+}Kk)PTPj z#-eky4EXm_ZXSpiw7~hJ4(t23E>*l86Y6>y`SY3+=X13p->q-5SsVcQypWx;uc_Sf z^XDUaKDbhtPr)ji9x78D%lKmlZmm#ev=tc-8~hN>Tzpi=^~-7my#MvyDAO&5BhK=j z0G7uV_APJHW)?u4DH%m zSG-A|TDZPxEbtqMcMORx{lFJH)2F%$%`bCK;4aa7eeZPsH99-~C+O#LWb1wV4h_!N z1nxri#4dcy8gTvSFxz2WpY;^Q+t=q{{zHEkxmF(KSM2S~#u4X#aNl8{8Onz^_~*kT z?Ju_j->I)|pj@K&qfz8c3-NwrSPJ|lj-9|SS845{o2xe+LG`XUl!>#WURBqQ|EWJd z3H>9E0q_S8T`H>eg#2pL2vvV>z0@T)wH>|XPKfVLh-)J5G7V=Z@_vGTKkZE{#>q~i zcf8vk^!G8zdAvP5%wT+}>TtOY{tx)@`EN&@7q2@JL70%;j6Lb9ac|{!iLY_S4>sta z_`#w0@kZ=*iPu@9Oa9Y;_TQ=;b!8ombxa@CJ0icAyxw=~vTHULI-vLx`tmKwWR0Ef zm2|VfEST5jU>2_?$bz7osU>9i{9(=`d8MMi*K09b*0n;u(2K&Nz!k;AlKOT?oGnaBiv+EKhu12uB2GrU$8HmJ-c`?w$O{1A-+Bp zxmHvevI*r&Y?4mYA?yb$xxe-js)t4HM$BAW?)NTjb_LXr-8HFnIBtiTf}>Uu@cgl+7)N}P*$wYw7IVYZ@2r2BqTM%FDd{izL<~ESPc92; zq>m#!U+g!^<|O%)o~!LSziME}X>i7Uer#%tdcjRG74Qkzhl*pkc-_P&7xi->-pq2U zf=2RWd(=}&``^O-57Qns5^b_mcAP#vd9di@|JLl;Q%Mg0Q$xyRSP1h{wqEU5q2Eb;mNm6*>4B=arUK^17o(uZ zbfWd^3+UgB(YE1T%ja*dZTwdq;qNK*23Nt}FGnn&%cAc$w>`Zn>53w2{E7MLq3&d-dvO%-hhlM~ z8$Ll&FM7x+swzHRd8dz8-trjy#kFEkoBaA&-)*O2f${^^x6S$tz=tkoe-Nqf@C{fY z8pE*211<%5B`*>r^;-s)T#~fdphbSH5$vs-5x2(aHZ0*)o0rrU11on3-7Yrp; z=Z~6!9ue$g599}Sn*{A}cNIPnOwZT6KDSIB;VD?&PKH5yaUJC|?>^!;i~RE1YFSIw zsz=Nm)~(Gbbf3upaOyFYhJD(E&bKHC@b3uDS;eo8s;IuGN%QdiaQIVnKwtL&=)abK z+`KXY_5ok455l|={t)U{2n)XlOyul7W9xi|oW6Z^s{QfF`r?J@Ps<=4G!smBG8h%Y z!>hmS6=iLF5Br6?9E5>Rt;-5M(~*7D>`^u152)Abcxo7JA%i$d~Jk;BOu z`mWSX-+(WRg+JI^Wt{coJ@~UnnvFzzd#6GUW~LlpN>`16`b7{SO~!%|o_{%W80 z?OOm}z&I4j3oV@`j<(t_2T}iBbhXWo#}C+c_l0#f>bF`wBH34{H!Ns~`T_nM`T<0v zcoloqhVy-8VoAR@zqn&i@f&CUh{cT?W&huwNRscV@k~bb0k?7h2tD5~t^=3(H6PDdmb3A`lw2o?DZjBD? zAF$u})wZHf6jg*bhX-4y6uni5cg#6oiOzd?s%pIJ;DAT+w1x&cKQwpgEMcWCJMrF3 zE{dmYpBx<7a^SfXjgx@lSyiZ<|CHZlx)>-LXkR&jD4jLO=>N{XqpmISKdEyz_WbJ& zdn{k9lgxMZ7UFQMRsI;0DF^f8FCG>T`2&A8qJH9?9I9_-Ln{=WG5yFb1|h~VXdb&G z71hTzsyF88l=r5YLl*+9o2SJe24^tLPMiALr+pjfVTfw=JKV^q-zhG1^6E0ts62oF zryT6#BW5sHT7f<()pd=$1&VKk5bMb|x%KaSr(N|W{ba7(hj5i2kC@L5-+*2^E2m^5 zT+eib)70P5R3Ca>Ry)D;e9CTVv)!Qg-3I+o@W(#Gy?FHd z)V(m9Ia^>JLS^pobw0$W>6#GGTiO=G zI_Xj{jNX?8$@d6x$){v#-_mVab{)|E^eoP6EN6Xu$~=zh9ojb4GSa6=1z(v9^L2M! zZv$eAg{kGK@m0E1ZY0%0zZC5|%Vb2Hf1z}{_0Wrtiwg{XOo=%k!j)A~y-9ua&$k$op^H`OHxUWXL9mAE1PCiTRY0{TVY zYZ=MQsVLtgq(#x=>8BsB`(&Dl>~A*Mn8799FAvDn9)j}({XBQJGx?sbT>FJ|GlY+{ z{rCQlejcMp^4Et+<;I3@_MD6k_60uB7%e!;E{JwD^Jhl<_DlOOw!PFtnu{1`j_Qqf z1b+c~ryvl!Y`5-O-@&@%O($yLe5nPwJ)-}j7j-M&s)h5(L-i;3qPMgHH>WyhlRv7D zBAU7mB{;(SWW(ZDeEE+S_GkPKbiFl{;6L+8v0LVj4_88b9^+JT$#VZchx|3> zYt&v5vn4+1@XGrVd>}MCQYvdeUaI0uI?8{-Lwj0q2B|qxoSJ~~-#P`HX4&}ci{RnF zKltp=me>$ZD&${d;oDo6%SmO9hcj!K==<1qaz$lNFJ;;l4LYNKEEek(xJyfq*jLx^ zxLe1|*3)rWD&m(F=z$Rz_?pFr!+h zw|@;33+Af5-2Z|3I)`q=AHC6x;ZNaxB>Zf`HF=#9%FPtJ-!mp|q%{3+UtQ=)+VEDz9`9mLsG3;@G5Z(`*|8K1>-1|#4V$`Aj z13jRyif1_24^^%A-uO!RbRoLCc<<)_Qjj#=s9m)yLMSqV%sZR-AA(COR#tRR@tq8otl;JzMV|IWprRI>?aLvEzp&n0M9UJPnd4AB! zB2?<|jLin%53}mDRIAYZ;2*wNvoCrdaGtS~(zfI^q{-7uZbN*5eA%4u;lJyFZa_C% z4EYG~9TcVBH1(~B?uh~X%AyDM)k(z_zp;AvqUIf3FZf5&IU2duILtqL3$kZ++Rj{t z0oh_`iP;gy1o-^0pmurzMg7=q*yl6~^WV3{St9m}qUx`T0gKm~=d3i#6gN*vUs%2E z1>`TF-y61U(?0xyRdU)vB9zZ!UybRdlRX=6yq@v3Tbft(@y4pvKVW`#wC&Ee8PF4O zcYXx>kf+&QfNx3g`JiHhC%A#rU0$f44e=ng7>6@%k`Lfr@GS-WjdKE?U*X{2w|*uj z9P-og`FsNKyD4eqxz_z~zknYEfxaT&-Kx{y0qmt&80yh#=X-8>UEwEo;H5!;}eoUn0wV|?pwWMEYuH#%?-Li@GiA6;3AIpm>DERiur>NPFj`ZxXN_qO0 zaQAq=v)5zEyxg^<9lrcE!`swvytxtR@IQU8Kl6`q6KYVuAIqb(k!arDeo~hJ{{p;? znCI;KA~G%E>mGZ+TVelMKFmifQF`2@DMBZ{ZUqX?` z%Uo{9K(g|CE(ZC<*sLEjuFh?lqx5L>dQ`u#{kQ{`CJk=~|9$0v_7Sjc>5cCOy)!@8 zuex!eCz+LAl(7K%br-LQ$FyR)qhI8Y&H}$}>f%Ow7b@~kRUW4F@rs?eV;}#t zW1p`5a896t_#L1RtT8MetEAk)pOnmR->&>vW|_`llJAPEY7k+ZIp1SDWdM+m&0=1ND4(#15DKP^ce*(1xj4hx?%e^_ zo*nrvh_BXuaZnNQEyCjp4|!J{T{hix}%oF|WMKL3|}XV#l`pu3MG85Jorqziw1k@WL?ojUQR&5Hc&D(eI2{5yUD zehyaSIcRJs@ju7EX{p3tG5D-#XaoLz+RB~(?vU&Adq3kF55x0lq*ZhVx+LrN1>-;X zN&1DMrW8Y*5G%fg{KeKDS{~km*VO+}2j98>h6C$=`JJ}?WCc&P+vGTicXmng<5#|e zCNb~gm>oC5;y;K5Rk9--$QPS`R%HP4Qf^0J-_zDd2h1x{<1aISunq9>*lQPIyxn;nd~xD1pEEU@nJ39^hkS0Trm~+ z0>JMGC3AU3(|2kbEssF{H73fJS+Ibz%LHaJU10yy*MHzVdd$5(=%FD)zP&N@S=xX5 z@hrDTEJJ)L$d|7cb^a|k$v#Cqze8iI?mw`-YT;BMyV3#!J`e0ySSbg5-4pE0o|U)J zycjKw{#@kz1(dvLkITP3Yb!+nf}-k->CQP}{0No7Ea<#*T*L3DSq-3s|0oAGxg4$Z4^ z!2h^atf&gm{Q`Ut(EB&5cie9Iu8uSMDnO!_8!O#OHu_DUWb&^k`nfq}gVx?hQgy^7 zSptODaEQ`U&fbIg#JK430&Oo#mkvq(#@3p!T#WdcfESX8+(;Y~XaCAfIlgRpRJB3x88<{oN=H)%O;i6hWfYifcDCXQ5to z6NLIkw!QOMR8svJ={J>h6Rt(4I8XeZRk3D&62y~kns4@>ury%FnV-R@HzT|_%*o%# z=n11czBUrgmjZr=Gr=owv0d#yHy&?1hh4v{G3<4Eu$MZu6ZtoeUt|_Zpxja&(o=RD z_FcKVzPg3>U-s4F0S^cM(jyA}YM$C#>Ca_1CHTX)o=&pc*FCQ%RNGKLh>#{j5~#0I zne^2oK|B_OlbW`>=Cl};<>B{1eCxsTirZY@sdFkkc1iS}M0t~%@$@48=cRwz03TtH zKz?7uGgmr4h4!u7cZV9vQ;+&AB~2`kzrn7W0K|_0e*dN#@g5>HE|w%5K1{LwH?m(Jvk^z2DKHj2UXYs6Ulm}FD@X7#>p=Ql%tP@K`! z;?Y%qC)CtsKb$XtDD>PpFE{%r?Pp%Z@fPTxmly~bQi@pWqnv>7@+As7z|LLFon=w7$uhCt}-ky44J$WkuZy4j~;tcGk^w$duIRlW72ssUW z59fzJ*yp?+NiJHN` zJ`VG~gS72=^p1;N7t=?}7CFYj`^oDh^HXA2K|xa_z|RIT;?+a^jy=gn(ugA-_NG2Htm!GHW+pHY{w9XNO%&>xp!9rXNb$>R32 z@O(FM36I(uxZJC?i?n;7K4_l~fB6lwo@EvgzC+@_QibHp zIGaU9_II-!$Hil8KWR`NX&A!(ed++`UnmL-;^62%?y=7=(u=>6VlMs=&C@oL z-|a>5!6HA2?(D7C#iFg$M$e1wF=5wkrYR6_Ik}VGrjGH@kM(e9 z|NEfS*)F3drkV3!HIGU<+GfY*=1nS>!Sy43X@`1)M3U4MZtY|6A3ZeRM8!M@8|_6# zfqN0YL0Cp=7YJlEiXVa2sz|7`tOAE?9u(Z^t<9e0yee$HrLu3rCTE6_e?W z?c~%ucOzFq{>}Shv7K449oMp`W>r%j;9-MQ*4*r@bX+;{FR`LTzalTEV2zAzlmcTnc=vF6N%rN^DzU6!RF{BiOS#bh(g^Adb@-ux6#+}Jevd>`V&iEJn9 zbCo_QRYYw+azH!C((U|;a?~F`v1Oe))I(4o8Ji3wo#M1HzZ^k)*xaC z@2AuLuI;UArj|Rg6xA~%Tdm8v(wtMDCvTzo$k14Q@5HziyZohZ-**=zvGTv06~TVL z2yQ#?9ON&8aJn4Vqw9_0tO1vZ{3ikJf7NO!JGq`x1|xJGr^dj;`Tu)M~07wPrHvUx8#lpoBxN9KI`Kvp06 z{lq?a|IIX0=YH5%mjC$pNFUfs5A6|MUz1_CSzo&^{J;LZe37fT>&v+@S2aiUJi}gg zbQHxd?)?{*MIpWy(bS~4_#0MczZl{>@G(iF9J*rgwUCqPz^{UNooh)_k-5^Y$pOf| z;PO>=LP$-XU{Sdd()oP-~SicFCq3NBUi_! z9QJhzv!0;Yi!ObNi%YA`@#3xv%C}z7H-$p}EQ+|qBa*7cakZZ}7fSH3dY+)LZs5uEGssu0X1UD| z@LT1FR$R7B|Dz#LET-0oCajEGRKPzTXsyKI{=}CB`}dg*#g!J&73_bAKo65ltKAIe zhwAZfJZGMA=HI)|RkDmfqyA5ju-(Z)7WCNA`C(HiQ>3MzQxX>$JuAfsLa`6N<@^+l ztePohC$Wzc`HAt03C7vZ8x#>9%~|`aNOW z3a7az!`RH+MpAl!jE^4uOU2Fk0l+mEymgq9%s&PRWy>kYa><9c8 zk=)l!dO|clcozEA7DL!FXR>R*+J0ud9n=?kx-OHgYKHq^`#GV+_)sWy%nJT!hcYn$YiY$`B{)cJ4 zg?9G*ik0`di*8%5pNaIokOQd9tMSjx-k-aNGOKfohb}|CN!8rQoaHmNrC=t|}TKkcu~ zkD{`=+gpWw;GY<{*K*U&bC9oq|Actn-{nr|{EH(%_GIq!?Rk8B#a~qMDNSOv%fqp#^G%GzYU3w=zV+eP4Ff+M%e+}#LXxl%?Ey}K%M*M z>W>5opCg)%!wL@rpToOzeE$~EW0K8kV+_~{gO_XU@Kx!B`A?e11`TI=h?@Z6X(IP* zXt#EL40^g+JfE;R4lXOPQV!wLPXUKiazLBGFkht?yN-rlAOgdd6@eWTOq@k@92 z4h@bQ8H-@P_)=rg;GQx;zx)p0QTJ~gjE2*YdOdeBOj2KSaB{4Uj-*Z;>xeh1XKG>{ znhaSv-sx>u7C?NlOfBv~`f%Zu+YM(RR@V%@oI zd(Yn(G5ueC{7ODKygv!m_Z$joq5_`4`0l#5SR5=A3zB6~Sq(_2(oiMhf@D^?IRZcqw&$pLmaC4)5Q-(@S%$*c7 zlK^YC_AgHmzVPT%_gx7eRgZ3$dr&rm+`Sy~BhVlFYRtf8^AtyaTd4zlOVBUY40Oob zkb3IH%o{i#-54=4J2z)=W#-xVTWB7Ia3UwmS&$Le)LRSvA?WvoafdcWMyWb{-2LBt zds>pA%am)^zA_oe_n^MbG-}7Er77OGGL1c;9k9!-B^dS-V(jUsEaU(7p~+s#;trOp zc;0yUv3tMYEJT9uu5?^M~v`Lj$gLD8+rAMBWg2OP`!ukA=yvxh|Zk> zV!)%Jo^A-*UfmXT0mJ$+z)OiE2`5AoVll^vban)NzLmQTc@4#RCDqeC65)N=h3uU6 zb6;O49#lwhv`5d2n5(<|=Brti5YY1he#-FG%Q7+@aT<3iq83yyXA~-jJ;Cp>A56Qu z>gd9wL$5%ea7xMnoBtq-^3STgHoIMnO1)4SgGls$^DrZx?;lFI8?AZi8>M3Re|#JA z6BepR%p5XPm(25oG8HwZ3O6f0e6=6+FKoSGUn|P59=W6|w<7XAhOqPvg+%?On_5}_EbdqdQYZaIgXsB(uO>TjiL!d9;&9(l{fYTIL|GVO3snR9x=blGaR}#gcxp`dfX5tBX?Sk!FF+ z5x1NpuVLO$lt^3XaaaeQAM}T^DV0S7{}>szFMrydY<7nk6Nt|D6OVhIsD87e;1kL% zl^ITYISz2%AfLq_G9<6(T$NS;`+|AwP>{oo?~b|6v$h5MpRQv04cp!DxiiXDO@Vop zF&gauq5pc!>^+Tj+Vv#2<{4((8WoAcm(Qy?1HL+FWpA_W&_&tzz7cMBT7F0Qrx%$o zRotQfV0;mLALN&mXV&}A{ISU>*cSC$hOt2#`n%4{9T~4WQGbJa;zuuiBT>FoYCY6X zz%O*y%zk0NzH))5@TDX0wJGIE^O6DE4|H6Z>p zb8rsKly{`fSH|{itvg1N=#@%O$_b=;9hq6hAF*bvkJ#4E=aV0j=Op|D%09C9pXtDF zZG6z6?Q89x0(!@vu$oUI6Z&S1BM@B^YGgMx7pbznHrnJVG5M{7re^i9KL+UZu97lr); z9P<-n`VW$X{x3W_`XD{pk1w=3#!=?jnz}P>5m0aXWtZB`{}I`e z`s5oQe@+r57jd=kUtKKyx4yr3rBhQOa{br1V{YJ2K`+9RIBs@O$;OZ45R0P&;S}}z zbsO;-BjIgQzz5l($Zh{nfaL~F_3-;a_b0})1XVKH8j{Z6+?AWBLL0p_w>)oDXmVyN z{QMNwJ~R`Us7Nweki3stLG@JFvEawz02Opc_SILg-+@6Fkk zCP@ho#|5TA{y)W2&L&gHCrWfLrStgxNZpAU{Ek1C8S`9;xB{&uLUYz&uI;hV!O~$@$*H*Z;GpsclyckFz)*l)SGgVqdcQMt=P7 z3ECls&X<%dL^x&Nn^?7Jb2N$<*c&vv&Y33B!pK`1Vuw9-9J=&i-g3F-g7ub@=>06d z(xhZ5-nDu|Q#La%dLHcc*KISi4xhH;kndE3Hjd0#@Q%rPsm!`zxIe5r>z?~Y z+r^T#z@CBcU1)^~?p%kHX#2+RpW&pfeznv~ds6=qfqaN#;A}BYE&IE=>QiJNx(VNAdCLRU zWtu48AYJ&b7_joER#3XhnoQkK@H`9+jBU_f;&7p}m|T40gw_~z2j*{|9Psx?d`#>` znjO1$EMui_(zmSy_~1>;E>sCcFNx&SBK-N}q<3m+Wrx*@8V|G~zD}Sc!TFN1b%p2D z2;gsrK>zWefL(k0rfBiP>FMXpIlvd&56D>3UR)cG*9LqjX!LvG-AtSG>EETFA^*6X zNSova)~9`4MY1kH^Wt6i5uRK%(9xxd_#nKS{>5e4LeiY<<8y7#0q;%r&QVbP82(Tp z=?=;lT{$ggJ`;HB|~humfb+ zqSzbeE71I|V3R9Au z`r;nUf45>^cek)`t9Ij*h>Rv>Pm4b-*Of2Be#(fQivz}PEy@4&=fyKt>F?p|KRM); z*|cxeXqO7=gMC<~1>eUamUy!(Spi%lK7TZ2rqq(4aOh}8sVd+LJVqrO-)OO({^Y!i z$3Bz9${~xq%im`f4F~G$>meW2v9r544Eg8_ckNAbVE>@Muc?l?MP*cd3cC^pdT4pH zF+QH5q(%Vzgn{-wwVBz@X6T>N{wuch)qi+p+OXS8%^~1(fxe~va#o~y5wfs1Ng0((9L^9PH(GXhEK!+4_xK3_G7jF~msP`KEy zr!wusgno+aI$?p#s;l|0J*O!!5BebI)40_WeWU9Qc<{%)q|lBgmYPYm@)gwYw4D#U zr=l!d`{A``^4zKgxgb;54!t1Yr`5RQHDpqZ+^j#_K9yP-U8q(M`BaSZNv!HU9iy&; ziprO4U3)=xbn8Tfmgs@%MUE&HXsOEvVaAiRi6w4g>y_s#j>gruOJ z1bq_d2l)=BQ2&svI&t{^f#e7GeNg{-UVEJ=>Q9pFkBnzUYU!$7KjaMY9{6vxUjz-Y zKdBvcOjOSqL{f3K?7trU{Gos)a$)i4^OY_ zPg4rv`Ny$NwY_F!r*s}}+j)2GeA5#T7o9oFw|;FJai~T6yGb}fdp(Rvsr=+iq%YmO z1nR1Sd(O4S6N!MYfZkw$Z$(7d_w@$aYxW>M8N=NIk8AL`?{IDH8szVTQrQ9Fn1|gp4EPgA z|7M^4G5cSh$~YlmbpNq>LX952Ogfu9xP_ zK*>E{ccuG>9KH8JKNi|tUWRnU*9f{u#y4h4)yJKi;-cp>bo$1lVNR$$N!#cT?{AEh zifh@cho_AFQ7uwA2K#OQ)Bnn9@-Y^^kt$B91Dl z);UtD(V#wr;sF=->z3<&NHFfY)rsy4?C&9HDf{|gJ*BM*_q%SSMD-HHo0Y<_OW2fs zi7(SS#v+%OZyngT;>rep1pO1F7t5CS^`P?k)}-*6ax!L%WcRtN9PnRbyqn55S~#CY zZs;Gih54dAbtnGaKN6bFA{cKkAK~*CZ`=r24k{Kw`2-{UPO}T);YQWpMFx#ezD-8) z!FlP1vZ9Ru;G=usee8a2%TdKIeRZBg^Iju8Z~8P9#%J_Yo#HZ|5AllUD>h=X8cYs3 z-ourU@4>zz z5TiSkLyAm1i2DY5MAZ}8<9s|D_w15S8y)bMszRqB4M(-oL8o^9K0(koav}CbFG^i~ z0QLy^Q}>YNstYZ&bg6Hszi!w^#5q&t?(SYxI*#yr13$k;HR?yb#5HwU;O}}F`&z86 zXUlD5@;|DYEo7SvYWE?1i`(Bj9v`^5kWkfooJm=P{q4Bt^R|5cUar0OZ09}A@Zm1V zuWje`ef>(K=m|0j@4F=G`=8CD^CaZoSZd~xrTFs@8QcRhLNB8O=lwk4{sSIIlj`~* zDf`tV4e4V~1^u?^^vWplo@+=a0_o7hGZC#*@^(;Uu2!1O1T4Fn&Cz{(du4Pl9V_(sj-}_eIR7a`-&Z zpFMAG&BDJK3%f883HdDW=})IJ#uLGt;ZN^Lrz&l~!EyBcL!YewnvC(zq5 zA=ti&NBjeW-Eov_5OdMq$SwrVD z_`lXM1GbQW=#%z!(Jc!a)1|O4m@#A|BNURsn4j~@Yy>oPv?eGf(}Y4^OPC05jbE*c z8|T%*`zhYp$%Ehjxwwf+Zadld_#xLDJq>+5QLQx8SdC&@qkpp<0(5 zor8$)#J!idih^0sAg1dG)}Nj1o9zKnoYuyS*~{^%;6KNCM=;jyKH|y`^R$NK8E#h9 zU<^6lbjVy>2aTv zq5^<7q5Zf~y6@k0d#+fKkpEx4&`lu=C~1{DMuLBV{X6E=GO|-Ph_gv^GMVsshiVA+ z7CJ?TVnOvY7W{SPUYu*iKe|48KHxv0|5VY;#C7Ib*_V%|pGb{s94QHqMDrf4cx7p@ z=eZhp7dA%VW4FZ@Z;90(%}O_krL8>R-U$li@wDpQqf!wbD`I06zzHaJ*a><(4%R(< zy`*jH&P?0FP)^~*X!0)1%RD5r~#$r6^ zV+NiRU%M6j4e(R$G?)lZZFF!?tqK;n^{r);KM&6Mv~%7N?0Es^;fK^a9$`D$!**q= zedaxYWm6j4HMUKX9q?fban%Ky`jB7E-cr~z8-(i5K>rqPC%?wX@?m!sIPXQCJ#9Zm zEL-|XN`5NBD-a)L+_iKHx4{iu(18TVj(3Z6t@cSa zoX1>k%5q0puo%PhQN1e4CmFi5le07JkKRfLcEIn${tb&3X^p?ty4sU8-KE{s*)|mi zV4r)!wjd>h<%@Hbhz>Gaq?x$$yJ$ln=$fqw2MW@T*m%?rlo z@M*I{Ae#H&r4vUvjIY0L=+MztZFI;iI)535_}V1BQ+Dt2%y%b!{j&&t{+fd3!a{lJ z$75k>VaT5a`Zl-dz7;S&t;8f5D;(R%h5O_+G`J%``37O%Ubpr$*8KVB+PqI?M8vnj zhTo&nB~BewG@6F`1o)Zv^>u{*5`JKneH_l0HOHu=X1mWmdU`mPy|%B70RG5KIk=?G?0$kZ z=!<}!gM$^H4^V3}L{mrcl^5~Hy|p4T*si-Z&|d`ox8#W?iO3;A(7+A2KYq?)2IFNBH{mfdmgcr95)mSMevofNxG=?_07u3liuLsXmJ6z6Pb@2D>`y zHNQxZPeXkUdS5Y;81l%o@=cLY|Fq5ru|p@P>N*mXMXnq3@f*Kp0( z8&h^|-VXK$`DB*1j%I{x`e)FS1O7E5)ZVL;w$9lG>!peC3z)|=Cy=Zka0(u9O2A$i zy|tK3n?c>BI(>OGFT}GK$|SS$m(%gF!eYSJyI`|kX@$E~7o3>vn_?Gl>Zsjh`(<|7 zAh3P66u%z#r6oN&X|&4w5NihP^;;@$pn*p2(rs%?FhTVl$xoBxRIs(hf2$YN6BVN* zH_W~z#+hxU+YL%le+HXOZ?hu)<{#y{aIpaRub?j)kMwGej%({`qxcj44gVZ-EIgfruEyxK?b zvW~W^!I7q&0sm{|C1KP!_NN$g6v{@h&pB?&qK9eDF)QU8Y(Fz$->e1 zIm271B`hZk#&-OV&)ZZGO%J?*@^{!@KqjjXrM0xjGy&c+#anCN+1cJ%DL;IDAojBE zfBF-7N7o1a41x1m;OQzhsE&QU{Qfkb5uWtx-v~OLi6>YqY<8T`Qe7~X*+GmB%1GQ7!eT#8z zxwMq$5YKP@#vJ79NniuYpT@{p)s)dD0H4G5@n9cHOvo|X$GCmqzoDO*8$&KT<(fXz zOB(|`rj-g$ad5H^<`~GiVzVIFNtK)S5!Qp#}Yo zJR3q#yVT4djIx5Afv>krcjqiF-mdFdJoN$2W4`wKii8m@iSGW&FNz;uDwc+6#Z`2ERk;TRMc18wYO=O#J9_^}kK)Ai;nVL7lL>r1_9|cN za+``)>VS&)?4QH7{)6ukwdeizk%J#wQi0zN`1Lyr_JLE6Ycusd(EY^DvB~N( z@1<)W1r9+!tg(RRUxf0Tf}NmezQe`h&sH|7rxzuNdmcbPxtCOKZ{FE?*XW+oe1=%v z?c#0M#7|CemsXm`TSAe&V)gte*&PSPR#e8XfPVtLx7B826EV8c!(+DYI=)5@USmGU zKb#{D=vqPjid8WmpPjS_E>fD0gY#QpT#S&QP#>0jQ)KebkDHGnXXmtw1Z_+uqWx#qT_G|qXZ}126hK0F}oP{nGPVWtIG#^|OZmpvkH8BwLXtgo$qo%M& zX?SLPd(vgP;S%I)Q=k`s->$r>M0xn|5a0gqcQiG3&qi<_7B3Xkhtw&e{lKu#6SnLH zHc6$^hM!G9J6c#|AhY?Y@+tvXap6o9E1nhs2BpPAU zIdT7TlkCbX6u)@MG2~_sR|e;C#a4J9z`w{VWKtSqy`C?oBR%lq`8WRWS|&dW5^^U{ z{2*QQ#@Q~1saTB6^-mYST;#RpWEK+J#ajA{s)Tlmcx0bQ;(nO--Yx5E1Nh?cQT;a_ zW=yrA^70T-s_|&^67YdY626KKzDv2SR_MG`TgufjzcVvbDu`e|zz^|uXSMwGA%@hG z4E5Pam9J26u;>=)xHYp1D4xPRI`ElBBZ~$m;QO%uy5b#6ZM$u9n}AkJta<5w_OD{N zRAeg}oA5t>*LI}mD(;FlG~F&m$=z3qS{&-nUzUJ+i>Ie8u=qOTQ7Xdsux{^E z@$3%=y*&w=b^lSha=yv2ZfaKM7MJux81CZ~HexRYqZX+n!fA~I{w1%LE|i^_wRL<5 zmnRH(_L{MtOBV_Y-rmk>XPY2MD|o$SV%6VhZV@59*2Ah;*_xNfco{EM?Y+g_rE&-<8XI4My#;vG zm@;E6Fd)Q??t9Td`cX|UNAU1*U-+WP$wwojs@-2v8iFw&kTP(GeWT&qp`(+$eY``-&?@^T)uBqG` z1boWwgk|nxP3Xr;sPD1kpx;j(-?>4HlSI6ciG|}IRZH3^Bh%$1V$4DJfg2WGaMCVh zb65D2di3+8BO&A^N=kZP8tAX;!2W0JQP8L0)qI}WU&hDx@8wCVkuB?jolB(LKY7qb ztoI#T;?5tqLy}pwFa~-OTB$CepB6~NwCfqW5FF4v-s1yI>@WSR0(`s_{DFPo>^5Fz zMpUnQWOeq#e2*3GtkIoJk|ZCGyFkt+-(t~~B(LqM{FqX?R<=11%@@A&$#6sG$KAJU zMV+eDid!vjf_`4#&7PWgO>g>|cFudSw5HNiJ`u&}ITDfi^!;*F;T{u4d9P~I^J9h>Lzf!+_ z`MIU>#kaknXG-$5xkly`wcdbvvR1~0P#wpa4XTOj#zRp*gwaF|!ElmW+P@qLL3ju3 zXKSQlMqaHh{|CQME~s>(ZNy^*`j_{Pe>$Oi&f&soj?41O{GkvmkGz(DUb(ctVxEXG zy+#iLA2s5|QyPKrw5Rmajxl%5UM*Dr!hVJAlnxf-mC}d;${%6hyieyrNxAb6 zwQ9zM(esD$`6;etG%3jchYUl>AM-Pgye4{Wtc9PSHzZn|AyQ;o+jS1DDM9#uo_$Bh zC6VJ~)t?>7Lw%_$j9G!-jRlji#%EZmXun2m#@wT~`fqh%KYKms zg;3{s3%8`CwJxS?^=X54Cm0)FrP!OI^B@tu zSb1IMa#Ry<$=-sH>TIf65!zpEHsdb{-)|+A`?1+lL-yRdqY4E&u&)m0Nszt$#=QCo z`YoVW1!^W54@)iS13c6pC#4aJ7#@_+pbO?BOlh?kx_#E0siTyH{X(naREM;EY3 zvYBe5z~3chy~E*fs|bm8Fn_^W%%v4c+&b!KtXam_Q?Ch#mT0b1(KEFgszLn)gPV%0 zvRD=%7tHjGwQK*+f49-w#B0+nlpn(8iF3-yZQ5u7lqW-*~*)L zD9HXm@xb7cr#dQ9?9R8Og7>FMs&2|D25mD0tZ534Mb z$<*QNtvUVx3f+2p4_@W~^g@d7hTF3m@m0BruIT+ky;$@s&=L0MFrr~!>f9oxg-E0b zFpO?pdU1(6Uk}1VkbGBAxYV$lk%t$R?-dR@ z;L`RNsz2(4y4Zy<-!z1EwkSVvQpo!qX#nzhEGIB*uS}HGFHZ&NXV(9ta{=kgD7;dP zBfNd3RsVq%hThk}UcJoUIJW)#{@S{{b|0Sx@`rC%=YyN#H;S7Q~^Kh^FDwR15A1q;0!;74ey2-WemT*GKN^nNv&I=UJb!dY&>qMXGf^ zpecdo^RP$Fa})@P#cw5bg;Bhz3e_aMNHSOYB%x6a_oInHe_2q-6kMmN+*8NjN7a`r zwv)T~U~aR)PyW1zV>ArVIb;W$7k%XCI~Qzh3A0*6ZqDZO+7OSI?PKaYVd1ibu#S6u z=H5|szJlkpQ{%5P5A;@Eqi1SiSP&0Zn6tBj2p?VUr;;=2ahb~BN0JtVeHo`aZ?vp% z7hc@5m7D-Pbc&0)BM>~HQtpWh{fX?Cg!XyJ{3AQJniYroVKq)J9aew7$%RNitW^%r-&)ey#uW*0WiR}F4 z>uz?pAvykyGwi!KFuXK)0OtRgj0AgYPxhNG;)}z2ke`fUk0!O5&8}5#Qv7oq{28Yv z;uW(5b2v4~+c6&TnYaY+?HUAIjVAG}4Y6MAxqtckbK~(-hehf??B@4Bo$_xiFBcDr z80WM0!@e+*@O*ah68V;L!9ZMHHl669aiPqhjG|C_yqV@(I0*x_s8+_9Db z_-$28y*1=Fp!a)3C8u%wA^O9ouZ9YfpK;~*bmk}71}$IMN&P7fdPMtCjH zL;7|(W+-VUQ~DI>#T;_%{sQy3yqfLH#Y!;GKEw!!{y<#!2MDolVWVBCGpq z${-&085(@LbAQ`ggRaJ+D3nhbq)AJV>05UwH@zLGzj)jc^Z`j)@`^88n*)4rDhH{Y zv;uscJ6|Wem)VE-0#yMT*?|rYv6r{cibK4F`~1ykrayBkNl_X4C%^~GTW8Zj#u;)R zR}(XjcY^Ua*xbued!A$^w*b#K#Jfkc8xlCNZEj=gd5EVl@AlGEq-&^mNIdqi736;g zY1Fc)2trA<7K>@@rq1WzDTFOWQ%^)FqR+!CSHx@xJ5i|LeNB zh0i~RZIY3}tl0cf7#{N$@}Kz{-#MG8Jo61DpL=A0FWZWRdfkkpzh&?+_3Q!)dEJq- zx+9R^!Mx{DKOZk^!VgBmVG*d;rxx$MmR2JRZ41W5DnUFLS@fmv%t>kvRX+5?Z8`z< zuUJi1`z;T&-35siq#yT2W0jD+iPntm)oBzT4Q|*pndR(?Uc9qy+6w9sMy@%lGvfXJ z?qz?#BVb;YXyWgSe@IQ5>b6Awlebp|$70>hd*VB{C%($+mjphgL=_Z7dbsLTnRKmbNz)*a z5ZjVdO{R_Gk^UFa+xtDr#~E+P0nl<=6cANP7jLx)1RU4Ny-&JfSz6F$)EiaLSA6sK0_$7PTYr z`XajBGZx|hF-acLkvZq?7+?9ifamvXbj?3EwyLekPar3*E8NW9)Nz{bKq;w+0?{I zl-T>tt7X3U!h0|uc!Vj8@m~M%st3yV0pDuwV&8rAw)4O@biV>G3T40T>HjU@nI3;% zgMRM2gO}JB@I!g4-FpwaSP<-9A-##3?LGs&8^*{lbD}C{zi}6q3s|<^@b`vDD$oz@ zJ`>%aDZP|w18{0_Q1*eH@27 z2>CYfRqbu?%o%Z;(jrfI|A0R*Yj>~Seds)Qj;Dw0r{>j5!L`EXtwf1nGo%+}5ZEwF z&|=dqYm^{f7B?|eH1ASacw_p%zdwrIL9dVr+38hrW3n6K?-V16Y=+Ny)9pDok`Cts z`oZ@?4*6{BS*`U0><8kly^Q$}Z(QwSId4Flx2GA~6H{gfqA}lof4mFz;{)9~EU4?` z1j3X8<6<8&|HBK$i18|>mZ+aX+Hp{$81pz?d6*u(LEu4Gq1Hm@BJg28fBJM0_@(`% z5i$s%&QY)Y96fNHz;mEqZQiM&f&1H%vH39g-w9GE zEh^!~2i>@H;?vWYY5e(3FBcVj_44U6V>-T|XDkPLy`UfMk}ClIzBj01g2Gr?7w2!Y zZD!{NugV0woe3x(KFgp>A^YMT&Bl=5h8yL|iG7873-lyfewIbVWTc)JxdQhS;vu{9 zt=W*GarPvfFMOZ)EJjA=$#k-kfp}Xf*q4Vi=BI6I#>|OSZ!{lo5c&=mUG_vSEO+_p z80eyI}$)H!bE#UKulPqZgbpLAi=h9YqJ=aBx(0pXid`*%cSuyv$ z7^H^ZQN6*vXSDkm3m+oD}LO8vo1`>%>@C=t&@97z6~vTTI~XR3!tj2YyN(N ztCZ@A`Yn(T<(M#KM&=$-J^=pF$}`Qj3+hb8uH*#`pnS6?sat_{>XD+z8l^7?A2cAl zP^CWn`Y1D4ss#RSiktXPW)|f=qh0r?AiCe&eN3=V@lqmb2<(Z)Sq!CNnB#49@nyAe zEAY3As?_JY>JhQGHCfT9-UIy~HT=;#D!gI38Okq7;rHw#Bl0XrKR<5+`yR*c6WVa| z_Swj&&gQw4ALr}Nclt)nP0g>|IW%&4Ox^2HU%bxsLFm8zs31%5^L057Jb{;C_*Js* zV+k*(7y37VrkU=J^PH?{3y1sy=D&z68R9ce?|RhFtPN)_5~#l`$Itui;q$XhZxdVg zx}}yKeDoLa!-lXoLdfLqZK_Gc@Hxcq5A=JND5EZ{dH!LxIf@UwB)U4me@o;y2`=0( znCBxm^mfxD9?(JW44%K27qIL_Joh>5ne$M2=UPO;GbgtF}@Z3rGEon$&VlVWyUz4uwrboKA8zGEM(7=PmRKwkXPuArX zyGNk=kTbjZf@MuOlSv=qF{XTOFMbjn$ro6p7;k@FT=gPvU2x9-G}E{OUPafjwn8t3q7{=Z^4Z?empr6 z`eDf{mD19^bOrojFVE5KP-D(-AEQ-TThRT5`5`vj=VU!S*98mq1NuC^&x_Q@@*S>R z^aj4l9QU;!i)=Qpt;H>c_X+X9_q&Zw!uOBPw+9L#UN&JXyvN*`g3WG9^U0_lskvEu zvQuc}r>g5Uk=X0Z|K{Vjsy$*7X4foTZt-1YB zkJI|y1aFh+XwZj-6qV!}yN=jjIb%HWjfwO-51d{Vf0{S@2-y#5{~g7OvJ?8}|Looa z`m_A=Rkjhb@#g&sIFhM)YGc^#7p+sW5+8f%By^t{c^=F_%6THZjGJUhX5l9FaMp72fG_vez6v322;Q8&_tCTonup*eLjAk!DBH0a5BX(H zGwD^HQnAd|!nsR&;=sQJeh7Y0N}dQV;1wbM zrvTs4WAdI_5^QH47DT<8yNvwx@)4RO#onBDCth3Q!oZC?85`n2e}2{Gf%kr=H!P%C z=N6~@;sD>hbH?YA{pq|@?mPU9TBoA^kmgyu#rukQ0LGi%gUf%Ysj;;8Pz4NG_1M!z*o25P^ zd_L!D4AS$i!L<-tbj*wyTX0vz)&^O-*u05H^Wu|%RcYWKr+5`I5p6G13#a3Xi1imD z`TSueE0G@pMVSF8K4R&XHY~i!i+g(KhR}Tpg#9nsn(Y$NEI(5?Z_qzCViB}P@X11P zMeuwagV304yzOJgl|SwO-IwYnQ(`uGulKLLf3M;5ZB*X%JB|Hen54`th5aFwPc_3Y zLI3l1ZZpT%{jjg8W9PaA)PMTEXrvD31NEa!*=@Y_v9d|> z@#-gHX&GX1b;aB}(${vaH$eFXm#A8>RAC%V@aBl~`50*mlMdyYN6-AWa7RokeP3MB ziN8?4|FgNSdZzAY-V|>@wWAYXIP9dhDYzj$u6p4g{*~U5DSt^D#m@i>wy%Q!#v5mk zQr*Q8cWtx5tXbmS;(eVAdkXl=I5yvdrS@Dx4s$wr2A+4ELCi95p?=ISAFc#`1oW%l zXxepmZOZtTvb7~liLl?nI?opR^MU!~CTHlkzC6`=ArkVh+KFwV0#NTPfL=N#;YCVD z^rk{KpRYE?Ji}y$4_AChGJ}0ab+FHYUytE%zOE?0fqkNwS;9A^D_>trf`0`(imIrg zhr60Qcmn3r;+T1ql*^!Zb8Dx?Y*2xnC(faRudh&ZbY{OU(o1Dj6R0v+mm$v7ob0-@ z9M2D;e!o_DK6w`{<8Ez`f9$wfD`m?Bm!ltY=jLsQ*RcOfdfb`7(=%^Oi3NP37pvQh z_u&>g^CqXjK8}tqdPs}ytbV(>@3iCtgwK#1G^6O|yxOI>2YrB#Um4>~8j>;O4|I=5 zClOw}c<&trLl)M$Gq4-_Ltx+L<>aCm9S5KN;Yq;{ZgrX-<C-DrS$O(#?6n5YzxZm|EBvX03aB2$-p^LstMlhW-@df3fCu&0c7ub)w1q~$UocJ4 zNw!GVaVq-){nP$a2V_t_R(sRy2Z8#rQg-f|CaUkTk+9#_DDH^+#hqzd8h|l;lioA64oEZ&DO8ESr~##R8K?mzy5UvyRRcWaxuZ4!?|i4{heCTpe|^7UbR zD)xp6B$j__PdkV9FXYJ2xEk%OypyNrb$f^#LgF!&*Y_@jn5L>l?1BFuI4}8LD)y@3 zfBMk-zbLPX6#%{+t(Vv77NITYot5w^5yfl&ydRU4($mVZArfEt@slVn@vB_hG)~hO zgYM9X8$o{<*OaYal|d zmRM)hfX9SEy~KiitLtRhqm0^3d(T4u5&9p6@)|t?Q|XWCs*ul2@!Y!YI67^wXUhF$ zjV~TcLHLnif16A3iLeuJ9&!uUi*Xs0Ue8tl>O29t2Pifgt zX-%aXEkbpB{@u`vIwv0$zN4SM{F!G+~u&iQ0`TNS{#OslkgteOK)iAaWJ% zYZYTZ4sXF`>sL0u_&c~a{#y0;zxVx4+xS-0@2WBPa_m(6!nAa)hx!2c_;lbi6l^^j zIA1D`>Rap)d$zAjrtDHCelO}z)~Hx=aH`7()pAQkz&=qvJZAkoL~O77U9g`wb3sW$ zg2L;X4@BJ`IRf#f8TK^9_!i_z7zO)wgkO0#MhbpprZr%?gB&4zR!D-*%e3U z@esZe=%>iCt7~5E-VpT&@VP#$-<4~f_&1s{%C8XqJj6&XFUb~tB=)fJ7E{dbe)ZW7 z*dI5)j5VEPrU4$|HQHlfKeJ!6$o=HW6Q~{ws?xyN1omw5Y&tn)1^5EiiK<@qUZ#!6 zc=HVLeXxF_rFGX_BI)tBvo?&Sy(pk=ltOx03KSOLEsb+EW{FZV!V51k-RI53q$sfe zx=;=A3BHX@J3;-$sUetCU4NJ>3fK<2qOT3pTDs`EVvM+96;(J-$md=B{R0*B`tz_0 z;9ti`0WE~}((4VU%{3rD1OD$3Z`<9gh3GR%h2_2D5!JbBg`1(jK)NH)xmcj$!ampD z-NhMQtQu*{yovA?Uf$}7=FTwexXvgE9AKQ zPv`EfuMiJ_Z)Khpa#x^9x>Af1h2{Zy$(kR2C_XN3+5DF{pU>y8dw109Q}l;LL9%92 z>8wJ{aXj!Ff~MHB#8~_Pp=cir4oSxp4V-M@4_6oQ`CRs|ZF=O|WcxQhjz#qlsnR#FT==koeZ@h`*r@-_ z|6iyrFY1&B>*{&x*yslxjzYv~| zLi$iUOneBN3<7?BJcIDm{g2rpb(;cY7{Yl?hf^JUDHhHy0Q?I>;;QQ!F08&u`=N0YfpWtO@^nmR4&o!}H6{zo8^qu*_%R z`M($|zjH+WMKzmiH(EJPzu@=BNd$9SYo9-Ud-(R|S!B=Hi$ZNCf*o;}H%4;8VBXNh zc3l^mH++3f!xiC`jKac#Y|(KUcF5!rz^^!gRd>{Aw6;X5P25iSe4uX-gAv`^zB78T zYgGx-V_{mq@9wtB@Jv@xM)NOK5xFvhWg~Lbm$bj3{|bBs39?;}^=7t>>^FG-%~84xk!^IqRd<@bbg8gthnwnOvHOe*7 zz#lZ(HX(PNJI7MhKNFT1X6oq!X1c5P0Rbox!g&^-){8Ye+1{VdVcddFO|vOw&TWtLXn&&5R&b|J~xb=n7gAHQEt=3RQK z_KvHfXHrWa_uc`1oluApE)DzdJTE>kx~VD0L342t`FBR5z3*VjmW^V{gJjNzZEe$KIGESm-Y|Y*xHRviWo+!R|KSxUwgvpOxxkNaek-{l7`*IE z=P@e6?-?rZp0$lhwdQ^e@q+wgrf2BV?K2-oGu{yF656!jdHo@M#J{nVSbt5NZ_h{G z8NMh;O-+;=hx{4zmix!Kl%;o@tFNUe3vHLMm){o>G&LV0xi9APKKOs*j5kIY3G3yH(6SfjxY^KYs%k+H{gMT2PQ?1^$&0o*Py6Gvd0bpOpdB?^BG6GUVoyt{R-w zXf~?1gPcE&j{er0`ba1p`~m3AB+jvac*_Y#1+iv8UulZrJ5jl?G{vHeuQo;Tn;Vuj zrmJyxYk0U^m=y-{hop{nyxwDb`ieB*BTz3zC=&w%*)mlMdNF*yN%)t$kr*-Nw_!6& zv|rgjS(8lLjen^=6#LuFg}Bm!Vt2^@XK#6R`ntgLj08rCS7YQqs`hVnA-BoE^QNfI z<0*2=9dTP{pq}XEd3dm~4XaLoxLJ4034?iYM)vd-t|$?PEfGqfJ2nYnfz{ zFuqcs()k7HoBJ*`7d8J@WW~pu$!w#9h)+D9{C$PUKh}gRSJc^Q?nrtI`?%oqlk6e7 zleVdKJEHwY*gl3+gQ+Qjn40=?~o2KcSr(*S@=4R)Ojt}4= zpTBG3xHs%MXLi{zf7;I;{P#F79EYc<4H>Z051bVni)bLzm{8AhZkyN!xgb1**VMwb zrsV$F9j?`d`YYTcFB?~>y{T=BFS>{L8$s1>v;AdVjA~z|h!xmxU}#LFV)Nmm=okLb zPwxT#pr372jNOK>PJLi6&wH^Knkf`pabxdG1ArgsjbVu<_}NbXwKs+@gu~a1ppRw7 zYWb92*=YV0ex6B6f^Gu-LSL!o=w_5(8z#~y#fncdI43CO{Q8*a3xv*B=F>eZ$AcLm z+I!bK@)&TYt^>O;ZO5Tr>9uHyY_4Y&N$W-PpFh$35XF}H=U4GZ!EL($FY)VM_t!Cz z)yE!9i$gpfC*2%uYbz0my|$UiLwGPZR4B6Owrt%U=MoXfr&_VyG0JqAR? zcL~hylHHsl>U-#XC;buHpAr43yJ!shjSgLQpeMat^P^T>3gX2$DWKilg7ZAJHbSip z^?PBTj2e{#`#HC~{^YI%{YjqORhh5#UM4tFu4wqmAbuC<-8aRQsZ>QJcdbGALBaf` zBN{oB3eJ~*z<-l8nY_l0?~1wzH?MzDEs*uZy>DCl3fA4ch?y9SLHrxub-3?CBQMu| zZ9LA8Zx`)tigai5*Dq$n^Hz-Wl+9T#A@ZtAW%>|5VV=7Bo({dtMr7`ooG{|=@m}YZ zTKQ}=c((phv6U~1zj-v=!2@jU} zdBf8*E_U~;|N4vOl4%A+cs>gBGbNhF#!YnRXegc=ERY;*TFA90#UZ^!Je+U7P?4RpyyTBH&YtkT zV81X;aTd?6GHgyb4gL%88zCEli(~Y6k7YLSExqghn?E-D|Kbo#Q9_sb`Bg#^`-)?CyiDGQs2qg`zhR8P7q;8Q>DrAq5O4gEn zDO*`)#@J$PG1OItWF1RFt1Jmw$22~_^US@!{_{%Bd_K?foacSs=Y8IX@%m@M^}c19 z&wEq;-kSpRJAS&Z5ZEN0(GBD}ObBry(DEyW^)~S5#rW3y(w+&iaNaV8`Xk^Mpgaok z7yJS!GoFB-=MG+>m!3OaNvle80e%Vmb+R7--!iY}mmJZ@uOBC~aieU5B3bsVvKBF* z*2OI6CFl`es1}ZI5Is)RvTe8e2lDgUpPWi96o1JEt`{#}{FD&6jmvQUeS<-L_}vip z?~ygTTip?##FLuel^HyCR5{^>2=Y@2@P2(KB2QQ~FGiGK->Io=mse(I78(A24fPzT z*UQbbIy>*BH1<-br{awGc$8rJxafqTN-4U}d`azVB5=QF*KOTPLiHiueaU>BWSW`e zMn^B?j|_etih_n8=o6`oPw4ZxNdXd~^e^Wn9e#g9zpoRAdI)f0G!_Ro|N^k=89 z5)fcTRK0V>!+l^lS50BHm*$5?kOQyq^_%{lKNS^~LJR+iRfql1xe(A$@M*j@XBTeu z{CT7wWO@tHP6uS>v^*;86SZ(XY};w|0`axq+_r3i^&G)l{c2}DmC#*>=L|vqKVKPV z*^-bDr}1N}Cd3a#@B@CSNVoX3r^+^c&IG-$MjZ`?`fvQZFHV5h3`eRDU0Aui`I1;d zlGqc#JI6-$`CdZ(!JVw>Tj0mVIB&l9%KRjcX)`ymK8Sd8qNV#Y+*TN~9-F(z&xhh) zg8xTD!FdB|z`v*7lM{?h^K%9}S4pfC=9Is z9Pl?2{Bg3(2CTxixwU*4fqKIc8#8Z+pIFUx(`tA=?NNOB5nFe65JPTtx(@Md5_1v~ z-mrbWl)@Gk;2-nF8mDmK4fcCR-FCe6&qI8>MNbzG`lE+4=6FKSLX8~u`a%EOoGKQx zdvk{77i&Hbz9wcEx++DA>;XX@Gme7RNlMl`T6)0k|=&L z!vuHJetmkbe8%T)UR8L@Q>FKD1Hk`DNuG2FZ49rc;5I)V>m|`RXp*TLP1V)+gY;Ox8=o_OQo)Q1U|iVoXz&w($KEH3FMWOiJPY z#Ieh8)UzL%a+R*A{sj6Ir2+qe&_}0z>CsUo|MllN5fVK+CF+v)YoLAs2dX=CKQ^tk zo-#Q`jQQp~ED8{Cso|ZqD4O zZ`=DUI_Po0552<3r7a#VoS3R)4gO9%e@m&09{hSf@!C@w@nOx| z^%`;+rC$LXzKQDYY!9`ik|i>XpnYP?sjX`dYI zYNI`;zt!XD_sMMY`MKZBt6laQus?@6aYDi?w>4Ti@9a7S>jUe>Ud_-r@jCCxuo}8g zm>VD1I++<&cAw0%$0)NB!S2|NG(t=PueE2U44aJkuQ4aV|87+{P-^}o^%v_J} zm%<_bEa>D)N_!AD=oE)X{*bQ>E_hm~b#{=uK%SW9lp?%W<2%Y_3w7106uRT!z8nTV zVx_qFtyLb@8;MAtTp5r0m0#*Cis_^}kEXCPRqZ^{`Mjoc+z|Dbsgz zhZ|RD$0PrO0PiDJwaa2fMg{%Vs2&$^e`zF(_UP`7XRVOmoEpO?7T|hdlG3zkR5Zk| zF=q0BMZOZdYKgj644xP2iS4xf)i-keOIazXKEMm?quPww!$T#0eH8na5uv+$vk`W(K zeQ=kelFH+a%qOUR4*2M<<(#=`vXpC!7ip7{XZvt_BIwBjYP*OU2(L}*XbG)c=Cmf| z*G2wl^8$U-mNlX{Ud*bpzCkM5uiCbBbq&d+p=#xF;PX`9;|&RhE$lAk-lJQmOyIn6 zlh=ToM%uc@UsqJo`onxx%JbD9R9S5yZYUr3Wk@9`nZIeY;U<_PzE&rJf2)yL$q-Md-ftZP(B!9bw1BWl8h%kwf)H z?2_))eT}IVLBSrs%Cw*x1;fZo+%75DFAF>ps3G%&wztHd{s!@M1PeJyJK*5D-YZFB z9_9BsefDMQ(!c&uNxuP~SIxmh_a8oYZheu_x06E%Klpc>fgjsQa;Z&)E2`J{rjYuG zielNu-)|ZtK1b)WF`JeVE!NK3F#_ui^QsCAvL$0)I~&jHRjki8o_MgyVvPHnsn|I7 z8Tc*b?Vdh{OA9`MkuHbSqaRkz{aZh~Nis++5pDx~j^8_`m>Djbq443*Px$}7``hi3 z&d7LJn-`&xhw9k@DauXiDo2GM)%bg$_=I=vE45I1tr4@^2CG<%X~=yPJ#z3n zzy4Ytj(dUlgX%*Es7-_hTwZQ>JNLK%YbXiXnh-p!vhM)A7Zv&qQ*gZ@it&6;F%o_M}uAG&b8h8^E* zA3%H>!up7Y&8((N)k-VAE~-18uxo#9hWOANRtLl%(EmGH5(kP?LZh0N0I#;r)p)B-AlDH*ywSsX1OM}Uow~`AdFyii=`^OB z0Kba)c)Ia;v5N`)>jkE+r~p2Dm{HH_I$ZUm{tVo&jZD9mwaWP`3$wp;B(#lzUTJgi z?F)ftWB+G2^>6>arP)kp-mrc*xl91`!joj*xe}8WiJ&6p!73E5{7Hm4s?580ak2N4 zgKppX^U%S6_vbrWraPQ3=$DY8TChdvZQ}H)40WXc?eyJK^I<>fSq`^iF{R>JVL0_C zciJx#H|JoY2A_ZEHV6A2wX*n&7py0o4`LJ1em2eGf~=iCnyI zVKs?PWgT$PxVx#x8}KgFBUTAWk0$SlvOTjE)n_Y-UJfE*FH?S`9wMN5XqcB!$!X8c zVa2^^kEYB|fIep9dda&t63@{8=JOf&-1+@$?+t!lySMHJe;!63&zvrsk>2-~_aR91 zaqN*pqu3yv_uJBj_6TqIyTH6ip}LU$+zwTUx9>3*Pd7L7?ZQ{L9-Kn^P5b^!YM16| zg(4QN1@&KW;tRy7DuWvH*#ffAA0$-+c&4?r)lb^n(=W6wymYgw!ksyo&*eOAb{h7} zB$G7lVQ3!l$E7j{K3?aMT9j$H=q|F{Dff;~o*|i*AxALox-^HrfPlIrx0TS(Prl5G z#eSz>7eMR73o9;tQSvoGxYMV+cO<1`c;A>fs^8Vbbsct=wZ~gouwxNl@X}vW0O~I& z9@JBcCE8*RpMw1a^{*@SdLkm7wBDntZKA!w`NR7Nz|YJD2Kahh;p@MWm2o^}O8R^2 zC*A0N@%Ljh)={S@0S1)(4E=tX-(cS4XiDtK3ZOF4`_f_EQ=~VYRcdKgc?MYYpdU4m(#%cUlegIm$8mkJyx>;2b@&3r16m)GMEdlqcu)DGt!HlY->1_& zkIm{c=+8BCMflsFqKpgwv?5qIzTh|F1FBP$%Pf^PmDR?{>zNn;9?f~jYHmLE$BC_P z2GRG|b~0}65!M&GA&@$a?niAuf_ZuAmU55n8zDY`o}cR4T()6U(K-GFW6KVf{f<6I z=tnvFU?<_TFsg6C{1;D;nandw{fwx(wDp&vpP5V}H=KPVS19D1e4{o)l?44@3(M`z z3qKY|9)Mr!W_#pUNtI@;H7P;y=EO19`|dqPjamIL-xeA#k$**+j2@x3B1zVsVAsB( zj1BWN{2Y4Jr(6p-(C=C2uXC*?u=a){KVQ<%+@ao-RqlCgOAykN1XQ@$6U#p6;tJfs zKLGA)Kf1nQiwCXpm3G^fot)(Di}4t9;pI+j@yi?ZJ|Mqp?=(CS<4Ql{ZfybcJxslU zpTcVV`br^Qf(3tvJ^6w3Mlc^f$Z~G;nr`bt&f+5Ue-%<7U*`s9HqAPq z_2yZ6c^K9YJrhX6n1HY5b*gr;EX|{i6h|I>;Ujuovv=+Nr#x=JavjNC3w?jzcr);` z`cR&42L1T(b7d8M+|5cvDembi$X{T7rI+zp@nBp0HMtHslk1=#hI-VYr4XO+K$aJp zcTc__oMmCZD{3xjdtaW0XEUk8J8pi#yGFU4?x7`^sxnwmarGDUM|R#n26zWkTNpC* zqI7)dn);St>3jh6XdP19(R|O4SI=L_q>5&3AEkf=&hs_TFAp8jBD!{7H21m*`DIN` zX#oxJ(FpUNinyfIAfr9=;nWWM-sG*76sRZZ##~}8t6Co?9kGgq`3K`zO|$ioO3>|C zbB0zw#AFb1>>_vqK7f2+piO^tP;^;M^O7#*G!E)}nD~NOYeV5&K3|{8z-3UIE|=^n z)m65mX9~%y=J0hJ|_2FfPVfMh;Jl?AnC$x6VlfQbFiL6OrL=Ad?CxIr5Pp$ z`FcSP-5J(%;TFpyUNljaw*|+9TuIU*{%c1h z+0i|eT@O3U-5tzobel&aEoF)E++J%-2Va^6@>%V*vIaB%xf}V*=rqxT1IyEkj5_m1;C_StyPaYtaj+L4W)y?OpB)`8v!Wr39CrdwrzQ z7UvJ~bC^pK9FZ3d-d@wEm@=}M!WbzgDWdN~8F#nUs!CzxMCPLJx9n+~b|38*EX1H$ z`GD;EjR7K1->8QE)`9oEjjt0PWjy5Y^JRwdv16TV4TdXE+*$p?VVezBH|M_as^3i4 z3TKpm27Y-+PiU86%l|UsMngFd+9=mjW+t$Cp4bNVHNdaKzS%<1AHA!ledA?SRKL@) zgZU85ge=`l6U-#p3wkzjQgQ4rFz>H+wK$4*)gcDQo+`c)D-zst9`Mux=(#;Q8wb~& z-^^vAdOJQbi`vB7Ay#r;HDT&kJM?osHDg9vuDdisJ_GB!P;)R7!;RdjZ9Mw;8S*pa zzA^k)RYg#QL z>#yf(>K;QTqr6t04X-e_@ECQV|FtmaH{O$-q|BCX*2~i`1^^xgzvKH&2IDil_PnSq1nyKE)SH>j zZSRj5A1dv8QqIrkd|H&B3hf!MzLp61hsGk?x@Jd&>sP;>IOiWKKv5ou-ZDPH^XBa$ zbzXt{5b&g)oh9*mN%48+JRct=+Z#UB911Cm{pASx+GH;_d2dfsS46_2_%_I&SUQG0 z?&MQnv6M%Z_t5#Q4tKm_KD2F>m~;<@>Isl<1qJk{Y_Coz&IWNcQ_ z-QbN4el{?Fm|#c@cqA)Z*x{w?WUzT=2qVj82>p`D&$2~)7iK9glz&!XJ0n>K@BrLL z2H7j_guiaOhvv_kz<%&9knBFY_Pj`1hhpAp&xTJX8@I##So5Q#??9+h_CMJsxIf1* zPu>dYUm|?{mJ5*;pj)Wh6LMN=7 z;Bx#kZ_%t_{U)_-GP52E<%G!2`j!Cy*~^Rni5(f+sqJpzEG|q(m;k7?SLKfN8tHQ zjA1I4al*3!@c;ME9lSSR7WphLK+<63U~OQqXcmm2KPUS?T%*Uh=j$`c(a zkKp{gCpQ#ON95Zo-b(qKqx^-N%vvgKGG)Fi=yEkd_3fN_bH&|zM^kqQ2q65)BbA98 z&Fy{~MTmHT`u(`2W5GfK!|8eA(gYOG{k^&~WKt-n!uT#tQHxi|m}=bd?XkD|HfP_&KBlR3mca3Ee= zI2g4_pZR>EcQK`Qcpu+yIWW7PZHV}wYMV@5^KyZQy`NeqAfH|cupAqAUlRH_!EA}* z>(MC6fi&Evu;R^hIG^UgAIq`a{8@*WU_@9E>fi99D$=N*_qkskUjqK4kLf}*HEg-O zcAlV^5Bl#{!~6W+BE6ct;SAAPDVAX01^$0CJf4v5LMiywln~9{s4sKPfG+x6id?_{rXN`{N?p@ZoAlL_}2CAtGzXR7tgi-1q5K2f00Vx{`7G!(dm^Z-mt z$*yI)h@>F#S!t9%a?6?wS`Kt7erT%AL-z;h)o|n$fz{af{R3JS_Ugn=z8`Y+`1Ik` z`@nyfS!U3}#;dBU%@iMj{kx!Z(o9!C;M(4*thMkyPN8`h`TG_7gpI;0Mi!3~_3X`I zUg#w6#ntwzw5jJXFP(fxe_;J!$@%DkM7Yn^^|Y27^NzSI&3((hB+81>OH@hP2L5fn zxa`7>d_LuJ&Td1)oi!)6W*9-d-xoGn9n#X=ELT}k`ivhR!q-r7KrP5_vz;H&y8r6& zzkU}lNYmXy@IG3}1cjhyM!PHB2B$7WKhpcJ{#fK|&|e~g>T~48UsD|hBInOAIS?4Oq>c<}XWT*E+-b z>%)isYNXUlv=Pep60Cb})m}7~Ka2DrKC{2i!v0`nsgO{=Qy`()a|7b5d~I8LyQl_X zu8va(zs*(WzGD&Y3|F{@T4VcAzxcs>)GkZM#}n=0p(YW)2iUe4(&UGpNt;eqBYsu; zkk0aQ#D_QmtTh4g5nSAWnc;4c4B5%<2M90v({_0{l&@f@sQ&th?sF`Rjw8&R(oba~ zehcOe`qI@08G~mo?6oNjyp05423-r4smn@tV z=f87n9oxNd=>EZyJwShv0L#3kBb!$)3c`K%^3%RUWiUVd)*Wh>FJF(BtwOZAcU*O9OMi^+ zMS~@XPZ+L2$SSLKiHg>T>>VD8o4Llt8kNKlA6A)d>2NRfpMoD>uZHmb?knj*%Vp%pNxkDm|b+wovWj8s5VVFP4mm{da z$E~Oz<(^~KmVG*9auwF^Pz5i3havu*ne$EP{*|Bg zScafqq5oin`(HXRWYLstP$ImO@roOJ6NEH=Bc$rI4UcFM%8*b9xM;#yPeCQ(%sPe;CN*{ZE1#K(}L%@Cb97SZIw$b z{NvY26S|k+KAbz4>=rTLGi6NvHT9?gX9RH?HLH;~L|J(=p^9S_r{3Tym zUgz+ypFNivZcUVv7wBF6ur}wq7W9KI+`78VKJfqk79v6=t5U9vCL+E|+qSIhW_^dS zv?FG$zrnM`(QJeI+$8wvbL@yF5U(%;xu`ghF1IM3GSHIsu1@+Z5&Sp&Z|(c(rUmhn zdLPTxld-UO_?B%Gj`SpWf(WCeD?!6RQSS)cZ;jaTJzzKcW(x*c6~Y5tKcg##YOA+C z84>eGcrL)Jpt@FaTkazoSCF4iJH0y~vtmW++ot?y5dRl+ENfN9XeyaIXXMiI+I*^H zP=62jJ4|!j*6{oF(wm`Q@A&FLJRye92fvYW88gruqE9r&{L4of6Ih*h;#{}d4NY|; z|1mXfDdE>j1BZOwi@>i4^J*eI%@UaUQ&ukx@sZ#szS~H3=fRjUrWlHk96#l7gYm&E z>AJzHUUf(6C3g0gxk+yAQ_t<`{CeC;Dz)TSZ^9!n`V{Klok1M<79-a8{HWCHPn>p19gj8>fL8PPm(Bx6+h!epLx7b4$gZI%s+ZqxU~97*z+4In|ibM z{r^1p^4LFD!rzZDi7WTBt+sA{w0X@m#M54;&Ar>ZUy1+yKyx9MpRYI>u$mI0ixfOr zselJZz`xvr(cPIA^++@l_=iTFhA|Fz{Eg++`(EH*0{+JjkByof-ckFbJ|Y3)VK4rE zlY+w2^(AYbef75%acpwBsoy`hz^NhkNqf{eHS_Zw5nGd0?pam{56t`f+7szQK0V&v zr`(bLJisZ7+U41L7H}Q{^EmsCS*oPg;F15fv%>OE` zk_Y`=ppOH6cB@=VOMm3_j$K<)b}dnAhqyul0-DR~+kU~nuTma;{`T0muA`A z@TuwznWdNH20w_8kdIIdJnVqprrVw#TcU$>ZX4nKk^Id zv^d9~Pqa6Db=SH4^ls7S&VmHn;Q4=EB^| z$#1;)`B1i3ChmY;`BY3nCaP~9)Ogp~%)*$LLW70(q5d<&T{7Yi*3+hrruV}AJHk9c z?YKE$Q#if5rDGqskMj(meeF9%BBgEfi$*T^-xnQJLU^`)`i668Kq&;*J)b#|IsVJUSR!$Fgl_WXDeqbm7Uh2bNnYeg?@F>SsmOFv`;h1?X zhK7`Lt;Jp9Q|S9J6J$XCkW=*6*6Im9UJ2>8U>^~dmKG1;C9Ql?`S;sI>L_=ihF4hs zy{$&7dF5HWrS34!ZzDH=f%OHIpf$wKTe8<+fz4qYSKU=fW{l^T^D5*Mh zGWgP$MR=cZKAd&7NlE^b)Ba0!8SrWg3;Pb1uU$=-Gva5DY?b#)&r5@T*5?g(fy`vuzd?v{Hv7oxB2RfQy;L&!fK}jO^nr+g3DCCzA>EMNIr9{ zVp>V;0e{{Idp?s&4cX&nOPf3Nah20mQKzMe8KEb5@@L>@Xe@lW%x+rqQNWXS>2QAg z0=5c*A?qiJ+CF+0yq{70eMzbT+bH2~o9G1meG>+LYD%6_VF_C0fEN^-Fq;;3smw$9 zI>A;J%D;7NmDfZ>Ok@51E1gk3&pd!LCt780t}cGE1b73(G+YSg#1%zGSF4voe?P|C zy-SLi;Q8NboT(zC`@GW8xL!hW(2l-!j~Mh14KckX^WW`XsF}Cc?wTqAK8yL3)TzGh z*;f{=+64Fy=AI6=Jvoz+@|nrk-}-wQyrL;~(trGU_yT`k2W!-nR`Ry=uW#;>J3d=w zebUSVe*Wi$S0S4<(0x-)S|k38Jtg|!*K0*So*>nm63tgtT5?Mbp}!0KU`Jp+%|c*f zr@1H6w=(m}x-C~&K3SSd$6$ZYS3>-vgmy^YJt_?RQ|mmZEbCd^i)|K!bakQVn=Sw0 zfk>s&_RW4lb+Lsygn#RcIKzvOk7zc5pD*-h-LDG!I!{LVhjx;rqQ(cIljqOamOwrh z^Zkut4ykN)2ZMq9c_2T`k=n9NcIafM=AzcHw9V35>A3H3^V^gWgbc z&mEfDnqb+_%^1pW{qJ3)v+=?BuVP8jP!Fs6yl)KZo!mfA^V+%Afyge# zc`;@}#@ilj^3B6*I%d09tVjUA9wqpE{b_ossAP5f*0u8pFLH01c@Q7E$CnE3jEPqK ze|=PUUi)~BRHOWpSKB&Rb{uT!kl^q@KIol>@HV@8i0sCk(C!h)=hawve|ulj#qB|l z{%+=>pRst$Lyu)|*IiTff&H!7Tayy?aJlwet>d$oYf$}<=~Pgg7G8Q=bF4c*+5pW< zK=r1dhT5KOdPq-2wwz~4P#ZTTxMnzpI9Om{TU;yXd$B8TuyPwvppvt{jZ#rOBc}+q>FJe8 zq`z^Wbc&TLhx_kIKY1n^g|FlWdymJ}jHPZb3nmw<)f~p@D`Sk|i z8T0uX3VU#I!vRgyoDOKu3B>R_4@yw*X6T;)l1}GZ}Poop>nKg zMcAc`E_NH-$IPWUqcs*b52yPo{@1o2@e?AW161pkUK78R_aS~aUZ%-{?bxs-zg}&z zBF!lEYVwnxfQNC0&6+6x^7SmRAg}Pb@9o2Z{jB<)>|)fsHOK20m!*LdNeV>@5cP8|v3MMf%Kk1>=s$ zSvY_5SQ2|TW%aKhHJ`sU`STJFSQ5)5&C+KdMb#iYf_aJOms)48{q;X`6x5HP-%kAZ z%$%Nbd&-*XJUL?PRg+wJ-i6w*xTFudjxY}k@PY)7T{z@zD+m4dRXKE2&xHJ3zcT>v z;}90#WUffq*`Izk_$tD4JZrHJ`MR+VEQv#e(28)4Boze@H2=MMy3;c{w&g$k`q4g7 zPSY6nLl2LU!^E(QnKr33$S(m;x-`5}UbA;%LipVy{{A6Fk?fS$R@}JiuCfo+TcxOG z0dp&uQy&6<^7Uv5FP~CNq;yzY57(pk!X-4cw9pAmuh?e_5Why4do0b(rv-IO97){J z)v+(~XrU2E?;&`WO?JKnehj7|=4f8)v`v`@yg!&{DUS2=6A*h_mzEFbZ-jebrI4hGNqX^CZo95y zGynY&<%n5AOE(G;Uyb*PmoyWy_}`YNYPJ=PX^gDv)bO9!qz?r*s*Uj9!7FQm&h~7O zc73e%5#a-vk6_7LHWtYyitmKa8^s^Y9Lc!76ypBgfP~^Ney>+&Th?h^<9@Fz=sx5o z7}Pg+bv-*SB>$D4U-+7`3zSCLG2dj)^Zls2nhZ5J46W0(o2ZtDd9~)mo-WjH(fOiQ zihsZ4Qdum@;L)hkg+KZHvQG<#e)74&1A#HrcY_h~Tl$%%EB3;B9JO*MfP z=YKzb`)~YPlV>^fcV(H)PSC^f^(xsH$4w>~vAM`UcugbrR2k-tdBkUY{Myz5m6bgz zr;TV5xse97LHznxNVmzjI#t;8_LkYHn0A=|>tVlI;2)J#)i*{{SNZzxdXkzM)4t}$ z1>i4-u(_6LtVi#D6{Basr#IykrNHMjy6=rfr(95fyf!0;TGBOXVKDSJ-~Wk|@cEVc z$~zn13gX|@U2Y|T|1$afTdWm({PrsBuRbQhsM+x8&*Zd>jov6;fWP_v@W>D1Q-7Dc zHun4IJqg=*vjzN^LTYPw!1F}*V&4fW;TB}h*N>kdUNvDZZsUJHeY7dcoYl^+U))zw zg!#@HvRhp-etr`Y(s3@bDO$F%Dgb_e1pKYc4M*2T_x&dK?}L6>GUc{~Rlrb_Yr(8| zv|0Us{Nw}K;{@8xX;@Mn(j2(J4 z=@Q(3ujc*78lG#En%oZTI7Biz1N$RMM@n+_5tf1QOC!kdKwm0C#jz*k{PV3g!hFN# z0u|>gh;R6-;@kpX4@^}Keo3}TFN=A0K2O;7h>Zoz;{v_-gE5^gK_;=rB@25n^oP(+ z(1v+O(El-tL-96BcX|Nc-#j<2JK;fsZq}# zv3n(A#~4uv?*=5yn;8<$1TAJgM=w|4_98y}(6&o${>inq*Y( z!$6NVJwu3d{^GO%dK1uJtq{oI?IgtHPyDG%`HB3I$Ql9y7w6MkN%GPGTAijF%Rn!! zJyW}19^JppQ)X_Rg6o@O82;k=Cp_C7JK}FbzFe)sF4zk9+c5VZM&tvjI^%?IjRyNr zzpNAV&kwaHJld5y?W69gWz`W9i}-_w$4FX$mnNA~Lb&Fp3pIUL9EK2{A=C6u5a)`w zrc%OcQ9VP)NoM!M7Y4G@8^0<>$DH_2Px$hFa$ahx3E0>69HJJ7-_(Qn(uCj2l79M%h&Nqdr#E#&4^qz=xPh*_3mXIYatcRbo&fak&v@d|`LD+9 zqEMFwD--0%4`4q-e^r$3l?#&#k`sBrmx7;=RE})fVcTF1+%53_zT-9B+nWf<3;h~o zc;50+W-`(I7_Dq`&59b>KcLUHdTDq-VW^_^hXsFrV72e4p@e+cJ^SQ)?@@huQXTv#v604(&VcnXrdoKa-Z7(xdvZ;yozm|zvCx1~3dzo|Xmm|PWuGXRR zd32+?;}M(A1K=l!RbQ#e$05AUJhk%9(`>=@VWkdcz^4^2eD@z_o3|4WsNZx%`7pNL z(wsFqU_fv1dmFks74rRqz=zKG1?64wNAp>j%WnX`J=*TEx5o&+&wMrbeHs}?3Jd>l z-3Y=zc-uY`LmH!ISyz^h_(k6o&~VS#_;vRUoWihu6fJU|G0N7Mb4NXCK^pl>Vqs z%ET0c_oq<&!27MK^QZqTzg90=qy>H;M2le$h#!@E zt!54dz4Dt@(6Z;pj~UN0NuSlJ4OEpTsDE(+cQprS0)ATX53hW2&cVNO;zOgdHN~gu zb1~`4Am~S0UVW=f+T&_vXRZPD_=QTztZK+l&dm9T`?@Mde7JP-OOcUJZ&u0JJ__F- zu7zf3+ItD~VjUqfvrvyFbPgzqe!O(d)3#fsHcSLJ=l_?Es#@^XsWzBTla7@M2sHk#{%Y-x{1SY=e`md1 zQ?@MSnLVO=p-CUj6P0}{a!bx3>n!Ap;GfxHqSpDp69yPIpP`BRBU^Re(GxXSU8}7| z`M=KPrla>F^d)8qwHGpSzrws&^8`Jv%wb{S0KJWeecSp! zK{!v17{f)M)lM)7apk0=`9$|U?F`Fw^`_so^` z&*JoN@90JKaQ|eR@=}AD*%OlLyE3X!zfY^HarW-!6(YoMfT#NKc5cl|a`xJb;i~W8 z^F{za5MI@@svfCaVb-PZ=TFPU3CNid{XCaO{!An|zuR>47mtU(zM1f*&Z1-w`u?6x zxrMEAN;eSx_b2Syt#`rHq5E-575YBfUW^-`o*vm{DWvcT)n5RgW#KIGx{^-kS0X&( zPq-&DdO%M+x>qc%4ft5*A&Sso*XDz+S1RYx{fwpXXwtZ0Vd*Q^yOAFKpvx|Is{=k))q0}6qN$;YR@qgM)F!fIp&vSB|-_F**70^I6zzih`=eT`ntVYkTeLoA={QMoQt4ZZ8!r$B^S58${Xqe?J!r$QM*2iv2 z#N{?WQhu`$<^y6nA!At-PR^O7{C`kAUfuMjHTY|Q|JtuxwkTg_o(#qX*q2r>&-HK2 zI0gMQlYbVN9w^KgzedY}d`fkGXu+n5A|TLv_n((Yub^FK%s%_!!ROmw7&W53KCGw8 z$#KxH!+Tx44e)1qAKA9W&7(zmKgl4Mlc9n1+|>uWO=w}!Cv81RC?4W>yp106_aBp;=EJ>Yo!YcN)2l#E( zbk;JIOlXkLdFAAnzW5rFU|r6E|37vvH$CBiA<6XB%WPMOx1f&!J@P<^;PWm0V)+~3-^3V9{Acfu+{Q|Eky9goF-_=`jJcgdOt95 za;Dz_n>w?z4dMp)oz3Cz3JZz`o!AM_*EK)XfA8H)S@7_qws?72`m3JSiaT@Ak1iw& z{c%Wt?nf7rWVclmed|(7I_z8ZQC(gF=Aj*7<=RG#^rldA3Q4a9W)=dyZ{CFX1?NZ3 z*5A|9W}u^Bf(iNTu>bd?g0}^U8{6AtHy@M9e=95%HPZo^k(261M?YKqTx7FOyn%^#XplVvMJTf zjS}<7atAuUpts1P=l`HZikBK8{sid@E&Nkc`%D~MAwSb1wr?y&=gDwil?LF6Me?5D zyehxmh+5he4|qQdHTQ)CDb!1qn^TH)LVIIc;r;@?W8qd_dnf$;n_*wON^t?98_DI` zghYrhV@w<9C!%+3EDFW}-hg>=3C2J#eUMc)UH_FIFL9ZTHy!HJTE4zs1N*O)8Fu@Z zy}9Ss5@+Q(c)k(ro{GLA`*QJ?{8#7NpngEUEJXW#xMK$=<(Lq{e>zp8M0$r;yP9%? zB;wP+e_cN%g@5CyG++`-{w_>mkj@<0C-|DaISM&JxTV?F{{!M55O-64Ve#ceEg$n@cdRw z^ma}&#>8x|_P&f2`~ReJ=UXk%A!f z{+U)C4W12lg~chS>k)tDZ`tj3#ZYIJ*HMKZ(Pl;q|N1c>?s5NSCSD=uXlg^0f_wnx zA%3br`7{e3BBQ`!oyvQ3$!N%Vk$O13U^B`ueRs~Ouja?wH$vhCOV;;_|45%g^G>j& zIa7+z-#e$p&&-AjB-VW|pv$s|_-^xRDQ)qm!I{wfslJ6yjd+d>ht93Ri_t=g8GWnlJjXQDIOuc4C> zPVC@DkID*PgvUqmd%J&I=srAIohz#gcn0`i$z6o;jTz1zhnEmu)6Vs}$)-;xmNq_! z6vdoNrG70mb2z_ixa~6Q`S)#t!ShaUyc=CI_>PWCkv|Gb}`BG7?)!}dEX#ZlGGUZD88w#ghY5^XB`R;qK zsVPOAmS>kG=An4S?HS9so$+CU^l?vJTU;o}{$Lk4ciGv9kv zHZQCSeP4{?*%?mF z5q?Vxcn+U8PuA?FN31@*w&e>W2H`h<%lG=t_hQ8!>!u!u`Vq{_kl7zsFkCD}I~mI7 z8!tEg`gOWps%&DczZd4yd@Le0qkMfAi@YLqmFp`fvpqD#|Mm4K*`oMVbHW(vF@5&S z*PjGIJr?>`yFJ*J72S)Nxg#N zN%g&VD^Ja}z|o#7b}<-b-WyT>%R$>>99=3e z%W%8|;sMl446>=}RwrP7JnaP^KT4T-n48}@S)x2T1o``1&HaMi^^IwTQK{=?k$%nJ ziJ{M8?@XT(+}T!l0r*Rp_o65xXCm~{tPs_&$a(Zev(AYDLcA`H?-#(llQBATZD@J+ z({6sf#J8;W?Ar=rypjB_UUeelylF1~eN6X`5gRvvzbG-m6V7j?WCm6^Rn#zt2Y+D< znBpY1g|u2g6%tsmDlkNDHdHyQ@b<~c>Cv6~?Ogn3qnK3(y6 zC3fF!*nKa2e~d|KH}mM+aZoH(7VZPkyXO|b%x~GssOa!1wBEdAc1d{C#dy_eVMg7> z7WDoxyA|e3O~2n_L1I@QHi*P73}G0P+oL4DX(XoosZw$&%yaXnDw^Mf^EB`4!YWp0 z{465NHpBWigMYpXm0IvP$f$W1@XDJ7Y>x~}obfrF)pUf7;y>^H&&*7Lztdeq6GGws zfO&#KE7Leu6YZLg8oRtVqq zKEZ`wKff;%T>5)qEtbLLWYnPi^9!N7l%W)7JGK?Q|C;+9lwFx;(<4LW!%#dVkH(Z{6l?~3!3(6wf2+;g7F(ZMxcS8d^zXf4UbcbrIA6^$b{TX3M0RH- zTmroC9wWJ#t3Ta)VCq;aMYIxz)BYH}GKlfWgy}hU-UeSI`#34~9QXxDMsun}Wdj&4 z?Za>%EY!9SdcnVkdQQBhhno68ju}^XcMg#Zj(i0FKJ5|m{Q?C_o`tlS)Hesf zgOiw7feE4hez$W$cWlyos%g5iO+3=KdsUx;`~mvC4-#lfs{N5a>K^2^iNg8R%)%*` zKcl^;u36&8YfY+bgyNSRH-hq_J}0F;Q`r{kwY-3Y!ILKN{C&JbFi(#gusK#iECJyq zJViOUF(9!f;Dc}^!ZTQ&XHQ4C-Ke)k=`M)pb2@$51R5?l$hYdd7d$^JpkbLaUikJB zY3}vbsaWuf* z=Jo~7pMS>a!}}Y-CkRoxZkpd`Z%Uume|_uV(}%vL-E%(p4Id{D@%0}M4gEERBtvFB zF2_Vis{H4F(IxM7vwg2m(-l z`hk1hX4!X=BwlQ`gZ@eAKML`xQa@p*d)cA!2EYH@=72R0W<$pOxTI2q=500_S-z+c zI4ZZkyc?}A##k9u8kn0IzooDp&f74z>?h^6knO{Xj*&>jA5}LGP~GQi>Emqas|ziE^~l68gqu&__(%BL^QHL`XwR;P9O_4G<2 zn;P_lK2LeL1wD_pFQG3|$YjHr?-oWTCU75~l=S;a{e+)Bd)#C4W zKLY6`u**1HUZc&!x&Gxjq?f~Ae%GuXCw@>$-6LevNDiCwBe`n=2j*Z#tt?w_4PK8B#5SFCQR z!>748RouRmn7lm5-5nUVu^vNuWM6{=EH(N`4MO$Yd!*+-80N}*uFqnuIn%Zh)jP=E zSU!sac1noqc?A9{IVnw(S=<)0aAs#ff zcWUza3#zY8d+FI4_LH5*5#Ff=|4~0HF%e^$I}Z4yMeV$o3bgeGqW`cereQyhG6`0jF|EnJ+8#napua1Ss3;&5DV9Es)*JjUx-6b1WCrxx z_aXj(nY&V)dUITYu_?VQZ*^>KMj?r>H=~3v!}CEus<${bJe18K=>8h2`|zC4w-sVd zXU|HD$f10fyjl6y0a}{$kG6;N2*2T7%47=HZ#n3?cm9m%VmLc9zYpEt0oUqF(|o$` zDqLu@;mRSa^U`N-yKZH0RCP&Fe7}C2w2)cW+g3lshm+%TP8TNRFyC88Uqbyl zT$lO}Z>gfy?%7C{NlyO1JLw^Ncs-8yi zu_lF%%b1%?U*75)3HW}P6BZ>-&z2AhVz#eH8S2uXZb%o+*~|5AW>_2mT)U({RC`hNp(`u}+F|=91M3 zQJd$U<|seIGjc-2kEmE-K{3uns6WZi*UfPE^)t&R+N9`M)5U+|{{xlTto|7AyQ=zJ zGy?zMLLjT1r)UEFCGTEGqnYRB=B08)5!COk?ZT#t&nh&WQ7Siwc+rYAbQ2X144!mf zUMq&`U%s}=$B5;FIM?W@YPb*IlixHMHmj#~88gd7(EZ3H1YEiJ$~3*t4EeViE?|vB zwWx>9B_a~@fS-Xs8}+p8wsS^QCjbxIE#AkVd53 z!gIv$U_K&3w}yI)6k^tMd}JN9th!!5M|kl5xW^W>A26x?&pSIQ8Z8qJ+&mT6_HSlA z+fcvX+su!ZaNqRf4NRM9>etFlR>+SyAGbMX757aI`l+j5aT%)-UkUxfiZn&j?~JmE zA%tJ6S#r5&Gz+C-+7fT^f8NvdYxaW!t|46IZC>AlS9w1-onFEMu-muWrz@sHUghoM z@b%^bL7%R-Z2=V7SB+TWvbz@6gZD(QYc(|Js!|ATnhP_0PgPW@L#Sc1-)| zt9<^L8uikfI8fC$y2=XeFR~5k=>cMV_1E>?f=K@ukW+wDIkGy7B76%He;fR1 z1Nwsu#{k%W8Kdi`?1fF8t%iAwYgt}@9%YX-_@9A%g2N-xsw;G+YVTVbF|+hgzWzmteArRMA*X@W@F9r)DMwc z(7q#I6zi&7xCHZc-?c-|qWk~W zw%Z%>Elya5zkSK}bn~5Ox_nW6hef;5infzvSvFepFuz|>pe5ih!904n$y73ni!HZL z^Vv_s?tHBjdm>ZZaQ9rOV5H)`mQ};`eQ|rTQT{|p z686Fa;0(C39v-4-1*|99W} zpC&88eD8a@19_UZs+VY096g(%Ya%DFgHjowdqkae+C?}k;={Te|6f9g7h z6N&w6`{~128j4?7F-`GM*>UMduNI3htq5=Qv^O}5>MgY%6JO8^1AafIBNN%2>Gbon ztW65shrFC^%!*#?MECOj0=-Zwr+Y=zkW`gDpAPxb7^CK0D^BY4*d_xrant-3fxYKB z1->~C8}3oRiJ_9yaU|@TL~j*^2j@4>fO;FlTK_JJ-tq(Zwg2Ix4tHNN<=O=JW&!9? z&u-hgn4vj<>JP($7E-suuwlxJ#spLk!+bjx=eG4=@A9~CQbi|h!9)B^d7;xBx8x3A z$WIK_s3WwIEY0yQsBi4K3wbz8<0B*;#ks%2Y-Yh9x=vtaCd~tE%^H8u%a~;JM#>dr zaT0_?|K4x9O~BW}n#x*&^MecFzP{juAEX00_5Bv2=?aJkFrVt}e2HSZfekf1bz6Hx|FDkovVUQ zx(o8FgE^I@N5<|CWDK*i>|lJb?0vSkt*!08|Cui0?yrBadHn5XzW&=XVfLnj%o0x(c7~knjeYv-%8hh zvLg47qhYU@q0oI&_UEh%~kYg9-{kdR7t0n z;yYBMuh0LD`UT-P@H#k_-nt3m);6%ecFOA<9J%obV`SnF!F)joF=42OdbjZ-x8JnTzqV zr%{_oT9eQ5sDDMN5urL;ZfNLaUpn>w_gVbxcXjVx2mTKFts*V6mQb%1Yq2|YAs-}j zIdh`j8}3Rr6r=OQ&d}vk62*IHvn4_ZPb1V4ZQX`#Zd(m=-4P!+>_&A*v(JKvj>T_h z!M;F0PdS&V*(x4?d?6V9et3ozOP4;<$M~G52znqG-~H$4JhqdVJVy|M{IQ>LS5kD$ z<=C;qTa!!Syg2nykv%l3^Qwdm=};d-e}@!UMa7k7-nkTW2mE(+5q>NV`S0R!{vN^n zaq+a+02YpKb@$4x64Dcb;Kml9c zt=9eZ9w)n64DvZ9B|l2XfMxJV>v9hX#TSB84&dE~Z&tfV1HK7*hF;r>NZkwKx77-5 z1?N+sqE6O-m5~>VpFr^hOK)pFZx`{k=4*i|@HN^g1>33AS=&{;C$Oi1Go@SCDXB?hp&f^fp8J zJ0+c^alPz*!*X&3_yg$g6%Y65>-KF|4t`_h3HzG8E5_78KggtCSEB&w`xEltH8brG zP<^X-3~iWSHPWL?l{#*?&vE1XB~_6B<1_9}@ywa5hkA*P5jAl~)!D#rf3+ooc=nvF zLi!v7@{bud%4dP^d$5bBo}G3)nGqtGXQ;jI`j4pkk|95nB!})BRzNS+I8v& zsh5#!jLw(g-ZjmuaBb*c{zurf#9F`?zF4|=xlH+TRv+-u4|iQh`(hh%m-IDY{{oxf zTAY=Yd#q`>vH|L~L)kX(1AB25t6;xW0CHj6=%*%Vw^D(ZsbRBiePu)`TY~A|iL}u>K z4AunrtH^EYK3Ru8V3UkD={Z*wOP-ObE5ZKQ3|9`7s%jh>f1A6~3&*GFr?9>q|Nd5H28tn%O@hEt{y`-(S^7hkg&||Cv{{j8oxyvvQ zABg-xNbJWjmdx71LuFH5sQv?eJrYNF&-GQ`U-|+*`d_|~A|L&IBH4MZRjX%rbp~fL zr15XnPDYXx?!Wo{D2cWl#v+^t%2z6SE(&M zF2;cGgz4*KeAuP$X64ky67=g;cH}=bJ$|F>^xby@$IdM4CcqYQVcssL8DF=q!cE$i7g?B=9plxC^xjIHCj;$8Bv zudN&SyRc7SrDemO`MzY^hyTtKhLf1x$Dp2m@wwda2eJp#))`q;&xILjQZ2%Tr62k5 zE}-+Qr>Y&nSYEG4J5L_yQ*t+RW97#Q>em)*CEyM33AHRo$KKa>SAZ`Ceg>PeOSrMC zg=`}g6j%dzVmm>V`Kv^AOXl=(27fPmXY83TMBd2U$`6bKvJgLjPX+ZLk$KBza8QK^ z_nS?~FvIvqCc7jLIn@FmO8v>?WGYL_fwS*R5#*OUVV;Vr${ZKTd31U>1I-g)Uf2H$ zCvV%K_4T5iHkv1ZFJ{@c`-YvfSY=Z3|KSw_(lz>X(C>x&7i4Y8+Oxf+cU*53(#PP} z{;>{U`pYtQ@7-urUxmB!Lrv%p%D8HA6_8KB{2M)9|MK{T0h{}8VIJoV^}qR&EVJ%F z*gtr{sq0^TWS?mV1BiFrdNtM&2a*`0Sz1-nnVEAS9q+ms_$1*Z&KkprRdLCB>mP!( zV_2?YvC}1(PZ-rEmLR>sef}(R(SGRnT-c=x@2h^sh76xa_!1K8?=%BoF9yaeU;F!CvryAHJ6H6nJx?6>nVoxt*Li}Y@ zqPjXeEn{9*0O0VAXOs>d+e5iPZ_W;J$Ec1F5)! zRVp=Bx2LEGp9^!<1btv~@_vg?^$72(RTI`;B6qG#p5RJCe;)L!gcT|F`K$IPdk&!d zaluvgXdtV(_J`zyCg}fyzoKQ0jm3P%lnwpBpG+D*`A9i_DSm5Tj5OE}@F&yoJwrVk zZ?c^4--7!D=SyGajdhb?zugJ+c?}-k+0rG+w9ji)#4de~H!ECS*Yjy14AyFQIeD(n zJV>tiejE59Jn1)2^s{y&eX<(0t}d$b7jZ|1>1e57Uc&ge{&7*RXgi9343co36Uy@eUAnI*#mxm|Ir#VuDAZ`1Kx-Fkvfud@6iEnVf@WmdS^;^GnGu za4YZV=+5Pi=C3%|2KVarLWO9}B zo!Ws_e83mD4MbW_%Sc+|R7~8ZrhAW3{Y^RZI&F!$>NV6Cj4{R~V~4y*DFf_ogP0)S zB5DHLRZMGQQ){xx=3TL;GT;zre8V!Ywuz&B6ZBDG-fzE}|HMT+)L)!hqAb;B@Gp_P z@dm#>-Ei`;9(h6kGv~^CU6gMa9hR-U$UkRLp|}D1N0wY(bTQs3_LzHK#*s#NAsuDLlR5$Q89Lbr{%IbjCgFXHliFgCkSxKmfzQV>+L)HD`n6V49|mn^Apc7JK8j85 zud&?u2oA-Aqkq?sX^^l%_y~>!YKE$t41~a?V!Rf`;d7FePWWPo?NM^ah ztsCw>yBE=Py43~pQw;Y@=<;)1sNK|AeVAw1=dULX`w92EZIRl2K%<%?Dy@EmaP7#orCIUf`L?IHTRfdSHB$87Y9fAuS1U? z{}5a*efi}fMl&&? zrhSU1m&U8nyf!6fSR+S+{6E*zJSyVH^P}Fe)-@mKDx!NmM*hdxHGiBJJG57LC}RQn zFGFuJp<_;&+b6p5qu}2;wRbt1LP8(7YDc;bG-Xr@>e&+9qVl56;QnQ~*p6I3KoE_^I2V z@AUAR;tV@YS1eXTp!aPkB}N_TxkRjf*m45$*SYXw<+c~EVwN$t!@*4_zbhbnR4O}9 zX$oy3LwxI_6h=znOzw(yZA1Olg9C&>I@MEReZsWsH_ZqW_aDqxpXMMcW49li4h8?+ zPjJ=LNf{!WbVO#}ou7ZuSn%^nJnHwU5idY|WAqa;d=vC^_ip;IsbU@SXGVpNCUgs# zyp)Umzfk>1Fn@PsCVO8=c{a82U;J`=*Oj@{t=d~sr=ha5+C_F6;TOgk>9LKCXBC1& zByR&=KWP{xm33Ui&zK@Fau)Vyz3?W3UOJqA_JhAB%29mZrMGK0Nv_;?n=FR>Cl=!S z@k7(^+r8X;G1D{F{m37Mx(1M&max*jpUtC+ngI`*SvtX9-r6T2@4m~q+p&kw<2T0f z$g)uXOcFdu-7~Hh`dy#L05633b}IRZetZex5IHBS9PtB)wl0GtQGF%F7(T-P7{S!3 z)?AwgPBl=fQ-RO1JUKRV;Y9kKJx+&;`jV?ZKN^F172Xx%I6HBdlD~;UioxZD>BTzn zCcZAz*Uf-OH8YuFyR0?);rY8^A9-~tU8%X+SE+WOc}1|96DQi9GtRF){uJNPJjc4 zeufUWEZE$aTqWgw_Uw`9`Hc2reC<7`55i4bEyGc~Hi~_uJrn*#X>9dD>#SX$U>wG2 zZ!w9*`cg)Qd|nIsi?4E}<5nBczFhG`{b<5_5o+m?GX$>l!ym{$FL-J0+jf56mdfbN zG<1H3I~{-Z+#j}{iJu+} znCd|Dx0s%gP}2{FGBH48vrEO=ySM7wdD>aZycvRxH~53=ElgsoOu27qYcSZ?81TKk z)Ds3$DzDX=d>cSWov*xTrqk zkys?Ar5L{HnH0o#=#Q;!VOp_MRER4AQ~*DjqdeKxjMI8W_ZmziqJ30`X$5gO4eQ3^ zmKDknFTWZcww3=on0Daice#OxqEL0?hr$*v zzGJavq#i^t^#i~jIbqj>y5rMQ{(ft};0N>cpiihtV!6d|!-iD6Y#_hEf&w~y4iWeI z`qti&khooq(?b4m?#e6T*d?3qpobIGGB)mZT_xe;x<;ERn8)njTu1-;duu|YtSR`r ze#RSWajX4Q zub}s(|IV`HZ_}jxSMhKk_Ho0Rdesi($L)_S(^hC>T6F{?(8r{OZ^+nN@)G4o7|c_U z=iWCHbAL%bS?fzaX7{h&xX1q42Kf0=f+L<9;Fc;o?My@a9AO{T_7a_$TRQ&lF2CFO zkm;T)>CrGb7xdsqh41<&0~vL>`0s*!+8Echp60e%et|3(pJnKIlzh z&eC1Sw?e%K`D}>SVOnX%jo?3P)3&oM(l%QpW9>Y`+sv9kJotltjB2H6`QlMR-JsVa zi>dfOPHAt9`^3LM$)#SVwJ0IUwpH7`fsRrV7@jA$7ITT5G#D2t!mcw-Q!~aN}xwDUSBGK z?3E%;we0R`R$l(mK7;TFLO*9m3Ef$x<6d^`Uq7G0D%98j{afB;ZtOO&H<%Az`?|IDfGusHq+tN~!zKkr1^DGv07_vb3|;BHg;%~ zcTrpv*!LPu z{BBV!&=*RxCMa3TW_gyK-zJ#XG`EhE%h`Cd^n<<}npYv*5y^3IJ0;7_wqn8i9b@!| z&)6Lqs#{z(RuuT_7~-R$E7==rCKjy(`9~_ZB3Xvrv2t>rtG{{v)|USJ%Z$0lvNK#v zv}n@>vu{(cuKk)0W2lF}CLnuTa3O~Q0BMo=%^ds_=&4k_^A6a(x6*gV`*Q+(#-j_i z%`){9G@aPczvlApcoY@K$klWbqo*K$VpF`WN<yMpLRGV`uYs6F4qC> zcWaj&OBj4QOu=SH)D%vvk4OB@nSCn~?v|h4{qXJUO#|36V^h|XvMCzUZxGG)TKJtX zGV@{w^sCx2y@Cn{erm$PVKz&U50LHtDXAJz>mWTnEjaHxB8oUz0;zlm&Ku?H8xnBm z^+dP+_~?O0--i$!`xfi#lbgO30_O>Qk0%`-iA`1&I$14gA$~9pMY%9^UH5a{re3Afypp^pD-HQ&VeGzK<^w zn73M!;r!lUQ3XVG;=%0!HGqf4z~47_?C-!G<&fuoNjDw)p@8`D!mtmcT^Xl{?2WJA z8{p+rm43g=cnRvQQO2EKAl&?AZJvC5lOP^=w-NK*l#3Hsvw+XQzDC!NA8o6KPBL$p z(2##%7}&LqR39nhxM-H6{s)2SPj-{?Gziwapsu>E&_-#}d3GAR(%64!K@R0(*xire zC0QBbZO*IuVLoLYa2YV=;K0Q%ZsHv;@*QOnsJubK9*xmGpPtu|JV{Wp5#VM2{{~R+t)MLjR^mZ(F7x<%= z36fgey4_ML7t*vL|K;UFKXzS*N_=9_4LQ$mR;S;%A^TldG!?lL>g`E_IL`6{f0h0~ z273oqp)$Lc*`1OAI}CTCx3k6E(n$(jj-mxo+0z!}T+=%45+ zvPbWSaOpaoUgWS^(PwZF-6w)L$$q9=vW>gU#{kd97fptFTx}dN=H|-`XY-1O3^@pqDhP zs=gy%x*>WqZGJS?YPj6l4fy@xcqY>c_WS;qkIEFBmb^cb)W!Vo{XehHz$W~l9-PEn zXgJ*=nWp^)2l38p-HC3FPCcj}AU^JUbiVJ>v-|2B5&-Y!E0Qq^oJT*Qu%*+!MkMg2zkM5Pe5$GH~`V0^-kLI2vB6 zC}q3p#%~N1zjz_GJr^yRPyarG`f0Gg`eLvB1qUHhs^1|N>W_ptyU?g2+>6TOiWQK5 z%`-;Hdvt$Em6ulW!Y=9Eq<>HJK>X?}ou*t9^udhNUE>FI0DEMI=L;C-F3uEPH< zvc_}+lY41CiB4Um76*DPdph})V?_?`hd_S<@N$gd9maM<`3b|D`9j@tBB5IQjvuOT z!pt2X<8JmuStRF7!RJjHd5c9X;(Fwdf5ei}e4>$;u!BL>ME!#SBk(Vvhob7z*U@Ai zmE;^ZTVz7+_-K<>$p?S<z88lU79x!*xyW>U4VYT zr+5m6{4o}!PY&GUp*4_o9q!{BE?*B{U5Xo#>GCq31H6aZ5S5cPR?Kba9n$ygphA9J zK-XQcS+Cx?ZVq>Mf8ZV`e`g1)KU3{A0k>Tfxe4=Kx^xmOPB8sCoa@e^8}_< zfeTZS$=c`0{;Eqv{7haTX>5GY zPbJO0xG8~stI1NYl+5b~3_n*A`D{=ynwD^-aV`#fGvK!PWve%MHERKt?+RsM%EEEHJtB$;UabBbxzZ~L$(feM&55{Xb%#D8w@UUGCbsf!e z37uI9^V(34guJHXM8-DJrcc(Gq5Bd>>bJ8}7t1T}TVE$qXB-l^w4yKfWlrDa5*=lANDT8Tzp z+>}tltc@h}htqmH$E@}}_f;;2`Vsac7qf;O9V5HEe0+I;Km65K%M@5iudfG{=F_lvd&X68w3Y z##<3Bo1OXE*iI%PA7@EEruk@NIvjm#{br7Zo^PkS-MSP$Z=v40vy#?91bYmv%A=>))K@+j@R{-hYE~Jl z_gG54w^e^g=mx$x>!1?H{WjNV<&%9c$@c%|b24%yGjW`apOk|d^M zvN!v0-qb<5p<_b`@F{_hmutqf*i!wmwXyQ#;I0gp_gDGsx%2O~Ei9T^H zG>>8&w76IlWonB5T6tS={wiJFfo{dd0}cHUUtzx0iGKOL{@xY)qt`Qc$&{E4Yj3Co zyw|wG2=P6DFKtLXp)UPVDd9Q(1o)#diaY(tI_l5%I%r9vd^P+I2$pcf0uBvs_v!$9 zGIAxCs0O^CzwLhNCpcdZ&|6O|Em?3`=_%kV#uh8$OoWm=CoOt`|4{AWP@s zFZ9W~TPeI_^+3Ode^j?xAN+S8#V7&iWR;#-o0ZuB`zSDi!gIaunN2r*pEa|QUI7F4 zC+%?iB719%kWjQt?yIlbE%wk~4bN_FJt5%Jq<0QnA*U1CA9Agr|1(K37pGMp#Wr9c zv=)$mpqN`*mgy=QSQTEhk9ZbmZ-QfkMPZc4Z0be$z5@(PHT5>-k8usRGFg56^Dj1b zevMyPz>HNyK4Z=D;rm=J?gD>+)td*m;Sm3VaB#c2CT)XZvc@>C9I82jrTdHA9kR?K zhfC&&AI&S=E)`3c$?@F8@%xuA8?-~6lG)I9^s8@Ep1L=hN0)sqny$U{q8;K3%riN5 z*@YcDlo(NYt4|F2&kp2N)Xx$T`P$eF`V=myt^qAEH6x-)OB^jQ-pAkrp-(tLT1Fz{x(k>NutHTbx5FZ!nab__m6{p$J{W^>W=n?+WKwZ{bh-9;?c&S~3q|AF0KQ2rA}lss}# zeb~3<*mFt5@5RoDQ0Y%#|KIAcBy|5^-hkL0H=@1ftZgu=e~ixrnLTUuRtStO_yGRu zJLR>UoKOV)RYF%hnonV4QGjpoPe@n0YJtB8{k;$|v+2us5{DEUd~F<1eChqZU34rh zJ*IsE{r!a~Yuuhjxjmmn2}@az*zo8zhQC1X@DACb1mXkWnRb74&)iupw85Cx(F?yH zMB0v{S*g!jG5{Yf>BkIxPdqExXgu(sJp!FC-@`hv#!*IF$ZD3^tOWWqoq87$-^6>c zS_7W9ufbLL!NaZVOg>6!oJRSQaXQDIS@c#1PYYND=LvcOj?r$KPeQ5sVBsK=lU1{ zLP+^zjy;*sYiSwRo061*p1>Qz5*gnIh8Pq=N5abtj zFE}#~Ro{6vyr-Tng!#QI%kL(I#YTlztH8m>D-lxCpMvY!AR#@2JRc*aQ~TMM@J{v53|i)FIP-a(jiAMPi~Z% z@5b)gxQgFO{~P7!SQ_v(?TL977suF;fAnLnYPps8m<{E^WzzzG;mN9&(fvTVy-8vJ zzx&}z{z#0kY}w$D+KcwP@?U+rDjXqPc6xAsP=_DbL&zg3>aTO>#gBF2(R@`yx@`PN zRmuv}5=sE7_ZpsT^R=Rx4q+omPZauF)$8m?tyfj2^Sx`LrQI6}YVno5sFrP{Yopm`pKQ!m+``6Wqpc>WzcFOy+(Jzm$~AfYEty`cl*6-6O3!Zkcw zcKL(sAgXV|n8ueKsP9bjf787xh}W@Z{%%z-ivl$Rt#^%4J90G#9;11TJJ-jy2=+Th z;Yx?jd#xI<_bGvTdW=#G`4NkcYm2;h8vN_rf(MS-iDMT?M#kPp_(j?mdv7gFdRG2(_oj zK4E%&Cg7PyV}vxioxk1-i|DkV1AV%=6{DJ}mcMv2%d+^{J@EH9Jj3D8Q@Qs$h$^~r z5~H#0MK}MQzr3_CbVTCm`HIhfe5Zcp&6wDBxODjI?tYMb@EPW5+6e{UXE@zV6~93b z4EAsC`=Di*z{NtARwx#Q^v$Q47x<0kEo1W#|FNxTUNcOJNwjx+m~7GU1p2dpCmg7z zmZmiqsIj1U&TI@@7wvJ^AuNL$x>!aRjs^ikiKHHYV{jmSKQ0ZtNpC~2rZV>bvAmUe*_Qoa~HGFPo2>O?{%csh+V`x82Ywy}A2d-s z4#UZ+hLS}a+QuEl1^eQPIreJ#X|s}rE6xe#Qx!Z&KZ9tNeyfkwf&U$&oZ(uMDA|QgZs!Cpg|_!KU)r}RPQT|P4`X~2C6!_)V zYOU3`wkKC8|4OPP{%1dbsl=z(-huejPVsE(F3wSkd-PjaIodzRP!{>VxD1DL81=&x%i)M?x!9Ow?(>3URaE;9qBNKn`qa@$o zPeS&NdElrHoI(1d4GC4K{%i2)J^w;8{CWuSeyOPk5$(6YwoxNWpT~1z!Co*%tzN1w zVV@`F>*a^?3x+wKNY>x^*z?5}z=IERc|owB$#Q)1XLlOJvuN}?2Y^aZa^5-$!%-kAh#Q!6IgZV(L!Dr^^o?U%W)kS_qmdmjJX>2e&5XIZDOWAfc z6+b>%-#NJro)`9eC|i;_V} z=Sp3quNSsh_;!iX4tNIeW1L-%4V#YZ6L0^5&m+LR`_<>YM_QllKNJdlJka-P4zN($ z*1e%WMy5zY*UE%!7o39f+jBEdb^%^FPdWJ9r<)qPF->XuPgHMWeJ&5=TQ78bi>i9U z`{eKndew6yq;7UFReb@^`)YW2o0Oc?Pie*n_3$Ft7Yh9dct60`7_)l{{&s-jn8WNY zRWV#_=-*}&2>D0s38MNAwu)4v4-eJf^%-Kn!ZmZB+toBd`~m(2iS}Zj&(-Cc(`rHR z`=b;UTzBVme;upJ7emiq-{)atLGGdS2enB0X~mY>pE7Fs@q%n+Y#l24l9VC zh1;obhkG=NJMg|mpeOc8o2m}@7vWjAv9>n&Uw({U^%b|V`d;t!0Ki*!qVqrerVuNn zya9d<DvDCb>2d#MDLiD<_8hzkNX3@a7TpNJK|&`<`kfAlHGr6t4lWaaJ+(o z%}vUqdHImt2~@i)|C8)U-Yq=eoLrGpMr|DdJ=J2n?(nI{Xdi3q)5I0la{LBd6>=DEx%Z=m zo@Kh@T{`GPV@46qiv#=ttOMc!@N1~Q$Z!~Ujk~}ff8$k!9*?+s?%9L9L!mad@cgW( z(z+JEK#!p)|L8$#MP4cFx1Qlsrpe(D4}rg?>SZY!w`JArCiX8lkNI$q#vE!Fn)O0({b`x1^Q1xtur@#7j#3KmXga z{#L(iCc+~Ku+KE{sk3?@y%+Eq=*Ql$6Bdft6xZoo76O02nc-4fH`Mb|H^9dbL-ks1 zs&{UjM)Ia+qKcY8U*U#akE7el=Ss(tS2o?#6WC|qo4~nX${Q(kKD>0G?1>z&pVP9e zhWR<@|3?;O3(MqKx%2IjKdTSK9XMn>QXwlEG=u7kT36+a_9A~gLrdxC)a-`E*(i7afSj-@xf4eWg`oaxUW z8M0%u{WQg>Ckp&V@*!XSCcG+o^{Oo}{{#D4nU>uoc7bA}RWI`Y^)3y4Qy*k>e82S^ zfPUov=H{fWuPCYO`S-jg3+lyMVVn~?CCMu(@iWvveOUTh1Kmn&q_fpy|Lr4eaGjkxu&Xb!f;#aH;>X;wy9*Ws_5k~K z+Y)3GUPn*-(-(m1o$xn!Ieeo~h5wg6@R!}QwXO+qJA6{*)nZmG^eF_|x;e0P;eFKl znJ$(j(8r)vf(tt7@`_hm{4rwsFe2I7T8~b zwU3XEX|?a!nR7*>C-)80l%GO<*tjL?kqCUh0g9{Fz8!IWX-W6qTHRH4p`W}J^cQ!A z*La})s14+Q&~L%#ik2MH728xejrR4_263r9H`?yaT0Ep7{FVTJh-Ew2oWNmAO7Jr* zhGo@K#(05uL;eN&JHpFVQi{^Ia)w9XdFS|dx~6<`x1Ji5U@WMvARlz<7WBXC z7e{(E1pVNHky+HEXcb=FBZM z$Ve(QOQ%Xf8{%Uv z$*PsuEkpfGEbABWvtj?Ky1Kn~8kaH%^-nXymF0En#iV$mW$QHLzw?+Q{nI$(CbiTi z4*KyBe=-hq^!1HwP5x9M1pnUx`w49?&|Z7SKlCsMe+d0bngyR0DV`8C7lizSVcqp= zcHkWqCR3vv>dQVX5Y6kkm`9xl?98SYA^`-BrkX4{> z1M_h-oY04~H9$g5M*byix4%70$83(&9N4VuWyfPa3%cl}bvz|4)m9DXh7ITov(j!!?r#e%~%aH&gEl~$lXC7Ru- z+naNMBsV;I(6{ci9pJOy_>92AN_xmY8byicbh(8-UUjw!@(GCdyRG~$_#A8qn)pnD z^BKidaP_+%H*ZhIhYYTtr#5Dldj0j2w{*p#i+oYgPw8LKjhr3)yiM*G;A@~?&`)cw zq8)mvtm6E`20ou(M|RawXL{5a_}tM(d`54v;;zfV-X!)1wZ5XrC!e$z-j0BNc+d1R zv(b6rQ)(l9!?`QZ!VRcNkIDbde?{y2-BabjKDc(uMp}-{(G^AsnXfYte!#oFU5MJN z!t77#-BWa?h8SMDnts~pxOK791rd< zmlteZthVi2?X-mm{C+p|XK}Q&MyX%%JI8LLdOS?ci}*NRg?Yi4EQRuS;|$P~S+vSi z+&4?yHA=0ZYVC%7X>(V613Wtre#t1L;{uO4OLOi@TRm~`H97212=5DPu(Y2Dw-LnG zw#T~E0@H`StqCX|gn84Ntr*s?AALx_m*lJepZ}NGTzC0Q9PA^JK=mD2gk1Ri$efcl z!fQxYo!zaBP4)fiPoO@wxs2deHXq(@rusD3aGUZYp#_@>fJgKsbJzc*Ucg=2oN zksPjEMtD*~8r`Cml~a6Iq<$3js}~HdiY1m`#RTCN65)MK8iL+2PHM#Of{dcCfd8=j zYLgYYriOTwd%|&@Lh^lQucwT;Yg-5BJi-L{{m$Nw=0|LTvSd~uDMxInj|>I3r4pZivvIQUyaU=L>+t2dF;7vz5nfzPGmDpsue zxMr86jXn6EP?(R$lRDT+YSw*9UKJkl%Kvc2d5xkiWjOSG4c&hVtK+`4d)?L+M*i6_ z-`eudy>}vt4<|3c^K!#tUF_}2l6H8EfJgTk3t_2R`VY&mU>GN_R|@Pq(|w?a1N*mH z)sE^VS(^OEk9=JnFKxwveU)`5yLOf!eFpba?`5?Se|)3n4ofG6yryK(=LWo(qEF3w z>v-Zmm#Q^@zF&yr;;G!Y+~Sm2jjMYH{w6uk{_u7R_ z&-{f$`%i>ho|i3#Ql0+Ab6;H(EtaaE)(jUH?5BJ4rXMd;?cns4As?;mYvt&;R+c-t zP-oqgzi49#_Md9ks;)l#wPIOTNgMWs!n_`5N!#Ppnbyp2O)IT+b(?X|Hl8TpE62 z^OuWs?EdZlS8unhDSfB=WF~dxQaA3h~ZGSh$I@mj3zq??Yfu5Kn&D z3Y)UIY{}J%HZ-t@Dt#Th*`xi7rYi1&?-PVqS7XWw32}$*`gCuQ(Y{KAZ|`UwG;vWEuH#seJsYF zyS90r`_ERWPx~>oHX@N|xbjZ6S}i;um!ROtrs77{4SbMYM)9EGOri+YvHM1Nc6v3^ z-=PEtOozDa1HB*jIc=={TUeZ=YSCz$VfpiokD66Oo~ET(RPy;(u6X;n$q4Xt{q5VH zF|Stoo<7V&^#|WWiW;wfeM|GL3~z{^EreL#1R0qmqvkyGw20?_-DE1bOfulS>nrIR zS-X&4&Wu|N&HnH~uh~V5P5G~fIt<@VW0#ktPn7%EK)-}i4SGRsBiIhC-L$r&7QjM2R9!I`oWo_U{gF3IlwbUu{(AS^78w(BqXiy z1pK3)k`dyyujE@)|Ip+%lutLrJhQ;bZBesc^PwK}p3I!=Azy&=HYf@uK)ubWcWrC? z=hO*G^6!PVU{9bopZ2!r%Ky|S`ZXTGd48+!5sCPQZK@}a1nkvustG^bcNGwW7l$(q zHgVzae>DnI^HhvTeD_SLT1@}+*XySxG8mIQse7v@`ZwA91^%X>*S^H!%!HFH($j+d zhB02YwiYg+N3Ap_z?%)Ps}4B6iklvHmP7qVBPVL4c(RaY+s=b`kUs;xAIsJ?zwh+A zX1xsYZjuo#TvFQmZDnZQKfgiz?W3g9FLoD!xZ_663h2*6KfG8Zk9^;)pFMlBPdAWw z?7#Us?_tL{uy;Efs}AQq z$FE)~}xl)~x_~ z!2E`Hw)OgG9}y+$c?qjJ=(v$VFkV8*UFWF7HxlY!s~_g*%}c})@0VL#Sb%(5G~%Oz zY5rR;-Z+NsN%|9;dC0xjkqGb0{d>0hp!Z#0RUKQ9D01`SPNzi3 zw-*>iM4#5~nH}Wk>yAH>xcN-w56L(#pR-3e;^Q&YpKYlA)t0L|l|Ec~=$C0e$q4cV zOmT_RzCpAh=gx_*Rj z{vSDA{3GH6ECik0aZS3t{)hc;Gn9`Sdy4417Z+KhND5HCB$2;2-MZL*3|kDf+Wm`z z>Mg>Ct0XoxR^GHz1J0x58|)WPz~Lh6S{OWr0M9PWDZM=C9LJFyRFcrWalpPOEd}hQ z_6G47XX+fx2QXF&MKqa-dqx-WJyr8t9<-0f!G1`S4|jtY=>FBF^y;Lg$xzGPIM%?| zMDvQ&o~1`^%a0rC(es9Qv^*~%lk0!{hIcpFx_B$wrJ}K&C$#4Cv)eo2{c~zV`Ufg| z?!J`#?d1UIl}y&VW{)iv$;YUc@RFM)QX8FZ7Z1a}hNZjbtE9kw`Y3m(%}jM$O<~_N zVNkDfc?(N)IXV5B3x+FimLolcor&p#_QdUrpc01eZ-e>XR$9Vytsl~R%usz>A38CX zUA*;O_I(E_h@ZgkaU=&4sQ0-=8%R?ZtbkweW{iiW7hX^?u-m?^Kl@={I`(izRHp9+ zMkoS39N2GVkN8}jFm%+}DZjM<_3A^e8E%V5_7Q{h|k%lL;I z3N|lfgMP5-gU`fe@W=LZhUU~4h4eICp`lCGNZ+NQE}NvKDW=N{%2Y+a&-2n$m-eyj z4akg|wTT6LC3SXpSB|Kt36H}2Z=bD)`jUnlsSqZ->O^?vf-6mEhAgINrSbmIZf~ck zuTQM5Oip$`BoNmK^=-J&xW4Q4;f1-&OA*aJw4{WB|MVx){*L!y*P-{paBXA$X)vT) zDoVFTcnj$1LVv_XovWyo`fvY4*p&70aa{d$?R~%#N?{*eU`}aoZ{5P@JVE~FxVJS< zx>QGD{5{}{?Q@`mtzO)EYHMAVfgj3muqb@juL}pZc1T&?66D8Gj=WxV6r z0xdmyz6kVScXZc4J`Vc1cRLRoX!N#ln(Od3g72rPQ5nEiAqu_Z{6C_uJRa)(dm~AN zZnw0jRSZK{mL!HsnVCzrAtZH+ENLNo8f~&fQWVNomYFd_vc$x+XH2q=HKuNLBgs0p zX?%X?J@`F!5*bDr~@=R61Mk746g_-DTi_GDg{?B51|kJqeA?e48> zZfz8?^9T9q4U3+ut|%oHH<4<48}h?W zOFst>{*}%29s%H;#u}2C9XFRHdqjBpPXDfJmc90G9ERY$3`R26!~GYsJ@fT@Co)I2 z9D9fde5p6}jUBX+ahQ?H{{nm+Z<6inWM639+ghu{nSTvV*H@aSe|6j=|WM>x>5)@(o0Guar zI@8G^r*3Dd?g<-IzZs|Q*1}V7(7ycs#1r{Tu?wOLgO>M|&OZ>GD#7>019s&_O$E+# z6E^64a}1SJD?TJ8`~Oa!ME`wQUcexK`4^+I-|1&1=e0VozM{hEQWLu;Ro*a&^;c{D zXCe6eNBA)ekzW+msC0yzvv_R_g9m)p{z=n2EGTjw*`4X+z4z>B?5buYrDWr>0ac{rC7M z&sW|9{b2_ogl^T`Y*E3l#d_dA)o~6sS3fP=TUUP4R1h*jx*{}r2R|BvQk<`i$ z&vX7`o{_|lDkT4t$fVOAm!dj~?n^|9jv_g5d1J-oku7MRx5kC}bMQgh?U>U;fyh4y z=EwOSm@ZLZUMjhNCUm@>(*{cu<~*ID&ZR+nTquTRh+o(!z9Ea|)x{|<_>n>LlWsl~ zRG@eWcrdQ0FRH?a!-nS#{*)`9^Ge&UtZB1r=tul2)T8zG;@?!dvYD-DKM&!AjQu!Y z>i*YFrrYI@d+u8x0IlQ;RN znY+~~L18L3Je&~r_^4 zWyI~fzI@78;%`OED|K{tS6sNeqA$sw_+S4{W^KxCZX}!^jF6V2u+x73?hQhF4(gXg z-k~$J7#~>Pu~z^uzwF|azh@{;G# zD$O5vr2`)SY7+n38Qb~S)L7%^0s!B`{sLBEZsP6R-f?5_y!K2HPQ0i%B(<29u;Tt` z607z7$Z`WL$v{({vgW3-msY)$2UvnTisnq&E zJ^x4j7G0Bs&dot|ZwJrMus^R><#6CR=!Z2@tsN z`A=tBYRkd`J?KaAzM1$+o7S)V!SU$@MMv)t~VV>6?_*q>*LPQ|Rv zFwM|d(FXd|NM1V*LqE?XniTKY_rBtYV)_rjn_UD0r}1$6f3g;t+X8;zOjI2-FkG~# zc2LtE@pW2t)O4RsidqNBqj|12fL8|yKD_~2@!f_3DYYZ;`LI8{eOxbY-OORk_AlV) z1cd1K($erx36CN0r2WX7|F+nxu|{Vmbqd?zRkTw!Sl(v8# zANDEWDUFw3By?_Y(}Md6{Gg4W&r6x?gi3}#oX^v=sYfY%xwX*$9iDNWU*}Fy*`_lz zG9U62;3H`%Wi#`Do-a;F&pK@EHXr=T1r-maRX_GGjkI*Jzv?hFF0L8xDAKm|M}I%B z$gyL=5#6EQcIdYXusG?KAZw%hcy91`_m9F0gcmnhj4Gs*i6ACBB zECGLWsTI+s0p+Fm8Go_LldX}g<85f4ji@$ZYl9K$Z$`Lk3M;Sw4S-c@G4iM47?5}r z{(^mhWkzOD4|ZWeI;Ab{OK&v%R-AzNfGU?9T)neeY=F0Bx+LEaDGV#VV$^jG&mZxF zCh1K2`kZ-Rx^-$s(RpGA*>LU1Q$0Q;87ahPnU=E(ibbw22CR<@Rum>Sgj!BbmFmHS5!;V-f>z@9JQBy4St5Kfqq(Krtlpa)`R?wKC!@FV zeedOaMzK({hSlpWQNKR&giN*uO{Mt9^FyUx1O(juME>+gZj4hjfpg88bg;8%_vP4nWWV(%78_&{IlW=G5--k&)E^;#w9 zC0}Sg4$$r398*O1tsaZ*s-p-hICe7nMBvx{sLaan8r9vf!8!WPn{Cj49@3Z1zKzSKis5T>E#4Hv{zpHark&q5NPP zN9!NN)*8*X`p7|gYc}-Hv(KwM$vfF|2hCFw$U@j}PFb@4fd?D-42#1~S2scas@^m% zkb?67&zDwc-#Tv$^RKdo{3R5-@+l15I=nDe3HoDN-wDC0_G9*3!$+HkDp5RQ=hJah zhm}jD*6bUT)Xzj0J7&>3%(d;xofMh)(AwR;a^eGFQ#LniMLL#Fok_75#YLI zg#4Al!tf9JZLM|sn>0ueuLZFGU?bCv#pvtT?UI)bYCdZ3yk<^>)!Ga=R-pIEPTluF ziG+LR}eQ+4`FEIa%+j7juuR|xL^UW!!NBS|R`a>Qb zX~k(71;Agy``cMwNNPB?^8O{pJA`M&!5g*8saJK5$ZWHa%wxt~eHLLbMZD2+JptW+ zPFreQn_(8GxqGi%_UkrmnA77$(>K)nGDpvc_ghEZ$;IJ~dNY4qTGtQxXPUZC8K*F6 zr}AiV0)HFgKiv3aytYs5*;o_M4}qV=)pq?|hoW{cb-?cn_$$Izddo&J&9t(pW-ewr z@$JK(h2Re}bKx?M@(X<*_S*^uPoX}2v-w>isy9XF-ZwZMey?{#eI_C*A^dE)#!YljMiM?|TL6|5D~}$(Cvqx=Q|i&>Kxg{oeI@hMsQbM@ik1 z{tJ~KS+Ia&|L^_rb5qWp`@Fv+&I)sMv)l2zfSNo8niaz zcBa<2N%W;`!`$rpiQ9!&6zCrjpFr5^MD#H@xVn410q`cwUq#!DGxgilW6Tx4qVtKl zhMj%%_@q|1tgboA=i-o!3Pr4arF+}P;Jm+`gnf{_R)x&QM=vucB>T9DOqokBvW6>$ zgEu`p;%kNcy+?OV8oo13HQbnWH}k%C;+$Z3pV_B9b6e#dDQ2#%Mny2;hScEm{xLCGxMC;0GlT!xiiGr$H3iCaN3q9l zxf5qjL%s%oW@tY%1ibPs9Tt;^UhQ`KPFro&9l^X+<%&klwa3ag8#Z@6O2d=R%>dGNbaz zg1{YnDHbkvs%_}`pBmk11o@+rQ>o*K^`$G9x@=KKcm+Gy+iCUSbVkJ|lM*-&z(=rT z3hT3!nVky2-x3Bm;mxDMuWa!1NDCS{XEzD{>7%=rI+?W%blpMob6kw$gKN{{@l#Sk zpQ6jf(z&0AUFRzk?z>t@`V(He83vpRz14qPHY5F21h;|gKwPG$c|SlW+Zv`>1&tTm zzrVmObxeYI1bXXm?z2yWx>ZVQBMC`(w7U!WaX3XW&-;^5zY)vZ*npRwduI4~e=Wj~ zCMm&>U&Zv)CMhZfM+4~w3@8T4jCyq~6?c)=wSd%g?&ubpf% zgm_Jve383-<;o%ZU00#ME#L%=@g0aa^#eS^DgjSGy-RA|(Ojo~>UcyC%7>gM_{*Bi zO#KaA9&Gq|{e%!-E|IZ(uTIaaTiY=5H&KdA)Fs-I17SL&TENE%I9|DhBw@+})9;Vh zO&6vT(}rCle?ojE5nEO2O#Q8UT9%cIAwRw^oZw_TwqwHu4@eX;L($H)m~MXT&(2x6 z-%;M!yVHgzw~sWN0-yC@0>h4Z=nyZ-7RskMcwRs8rFUvBa$H=#=*dDqynDcMn9mEn zVDLIfrN8b{Y3}u{@hJZcv_>jDJJGlT>AU2%19EG;mj>T&nBxt6j{9#uocmVNaGtsd zTFe&QgDq*JwpmSRet?kL(#kJiWt=z>sbUNHB*OP*p&RT7TvHP^v_aLF60aaCb5Acd`t{Gb-+Dx%}BDintIJKje<` z;f#xt%z%5=HlJfN4sc$CCccrCMS7(Lfd`j|T79|BOh2`P4g0t+jQ?_wdF2s{<65g!K@1%$9{=NHk6}k`9mU_yrHz8{RaN4~85ZOd zp$Q4^ckI~3zEu1c63icV65f*V6l&h$oi6uBcRYzROTmZ1zA!BEPRKXVM;i4*{d|Fn zt>lb;c*+ER7tMQr`?esx;O-vN5qN(C)L16;+ea(I_)Z(p^Q(#8$oo)nxHO7aD@64G z)$mewf4|Jq>$x74fNzE)qNC@@p*!n>~k}TBM4j`{6iVTWCd5B(j0Sn=Yrx7XMa7>E{O81f_?;A_H{XafpvGGsUh7V}pb`^!hV=x*k zlSoei|3W{p07v6-``Vd7ero7EMG&hF;_^}+$|`;dtkVxNa|zhti1?4sXABhne}7ub z&h{VN<=`h{z48FeBg4F8c(0S8gx?5bR`<{Dt@+UsX9MT0i*vQ&cH&;6NBa-8dxuUZ z*5*s*GsM(SJ}wdPc~dp9^W_Z8Q58P zo{&M_5A_g+jkT)DK)mbZR48vR@5}rn{8$6jQ{-t=C+N>wlcm;M&lW>GpAyB=;^ID! zXX~CjGzs{;k&{yI;2`w07D#9NZ$tR4dV{!}-gn$#*cS3N%I`wG%Zt88AJFT#3GeeC zwqx!_Wrc@rM|C=n!JmP@$d8N5rx*uyFI$fCHT4>OLo7Y@Oh~OWhJJsx!Qrznl_FyA zOY=NYer4B3x-(_*If~WUrzGbsc#UqN)lY3=XL`%0yjWFS z=LdeBPs0gQs%ZWa&M&St=r^rLs~c~6JG$?2whJx;zs?$8mzqTQeDFJOx6}G9c^h%^ zxB&58>@C1YQ~YU7q|A?!^S=*=!x#s%gf90beksQJar6N4->Npvs!h-Co%>n@&$6+& zXrqk1Jq76jIN>?D+Lca6GPZv6xw|Ib(qS^j;VbwHG7L*~0N-_RX3p~dekN7oU*Urx z|3j)=ZhdiIr*?Nw%`bQ#{e%H*hrfbb;(z*Zb?@q*(94wy9#{Uic z)yoa_cFlnWHA!@o&mlgO`ajCp2Gkfs|6@Cs1A5T-_#-A|9}dI*s9esM5*qO9zb=q@ zgYu#4PaC0#{7`$00zyyE7C8J&ZBBX>EsIHP`EP2jky4)ljDvUFn~3o$AcH#qK(?t7$IJa2EjD!uRuF8O=jI*6y=2oxDxzEDl6d2zxh+-EMwm(o1WqsHDk zl6DvF!*pc9&q2lmwXD&bcUCqXfP8zfp_g9x;miC3XXe*^gnicaMr~*(LLhayoTPq# z%hJqQuU5=#Mt)s@7hXGUcX*lLbke!g2K_!^(K=+xx}}B|znhicm30TdgVeI#L2*qu zp;7_xA7G{0{2Pwhy$KYH#5D(+9x@Ofr3O1WjK1XVVxF2k;4O9k*@~ux*FnFr zEAxG~GRk*^bbfh2vt?U%ZPg>-UxX1MPCU&GgIl>VvYWv#0{oq47Raqjbnfdz634{KUHfi63s!Z1Jz~cg}Er z`>DYdWr@o7OEj^6hM+#})=W_2+dmP0%g z{lYWq`%(Xt6L++$6$Zu+L$qvd)V&0$twCrgw7!?5fkK4{vw-ab}x}Z6NSJaSbgM`cZ0k zbgJ<1^I*P;Im}AzzTf*%R}iIguJpC40DT^3cLYHP%@0LHf2S|i?btQ4gpmyM7{joC z<{MPQHFe}&Uz!k~-gQ-q7?Jfp)OYv$Xbd|2p&hIU)`WZl`)4MLqFLs5CwfYy zH41ZC$49+Uy?cSnPkadRURV|50uStbmw$%7#2>~q*nZ$a!pMS93mo9##;Gb-e_maS zEX+?9!+8h)YA+UD{*p_^=&iM@5dX~fW$v7Fe7Tyr)^R@Kt0G+S>-$A})`6khc8UM0 zXS+i}=Iu+zDWcOT9>9LM3Vr>g)0L|$phD_1nF}~TlO6FJE19T zy0Q@XHX-yUu75bkpY{=!1WD>GugRtPMKZFsv~$Fm?%djk+B7oE`%o=Xz6#|Hf~;Ns z-3L&U8|ZEb=T9pZyK%Td1|RvY&BaTi--CYcpJETD@L=(;gO(^?OY{LEuYr@NeIIni zx;=@l8OG)r1>7>6Gxw;M@C&jdwui>HIB$gaHG>)A_`gaIbE>qDDWZDV)LETt70CWW zx97zXN&h-_wEj#-rk?bt;XCqsZ{&M9`~!XpQ@_5}UpSdvApm`pN(H};H25TsVu|84 zmb$yOy{#$nk&96f#245Hp<_?+_)X=}$_%FMIe4D=?fttIr=o*oOa_w9{m&mLQ-8jF z{*SwV23d5psGfsZf* zYY=%;{E>LE;(?!tUx=hP&~pdKNwT~hndto)#}VBt`OPK7-*@&uySqKF2M#0So$cFz z*#h8ej5@)8*5akQOftUgZHD|nGUYyVe<0NB;$uH6jiA2?^H|DH19FaLHQhewgz~A_ zXrm(Il)w=8U_=w*8fI8;0)w*xc~dd zTF7MSQ-SIOB1t@sX5|iJ+3cX=;CR3@;76K(<9;CFzQoo(NBvmL(Ef4hyEJ^oX8hR5 zb->%;m-c)>olx=6P6gga2kfs8b)Q;(<571t6Z!eDEyI0YGKaR_f3Y(M@W_;j<(t>K zN(aoPK6Hj8Eou2L9xQs;T*)g(c+|v&GOKU>t$r)>3=a7FMuJl`>-aO|Z}g@Lm_y^1_*B^7C5H54!{4d~|bCY#Lk(S0$8F0ym8$A^%$B7eSrT zIsW1(;x|>#Lj0X#r}1sWW>z;%YP+0*?Xgs%|{~(R7u!zQ37^;}r7CulW{cgwNj557@6YsGi6uT@1ZNc)s7UG#8&e^S;X7 zT*4d0_3lVV`?iNg=9#~4s7b9@>c7pIa!1n7elRZ3MtmyunoJb^u#fIcda)4s9kH+F zYCJY-D4y7@2KmHc(&SA$uT`&2-Dj}^%)>yv;Zk#`b>j(k<$%h>Ht^dZJo$t4pwE3- ziRGz_h#w@hUy6#W)!6`EL8+?_Pp4%`_|Q}WHhx+S_fX1NH&Tz|YI}u{$NG9d9N#LB14)CZsx&LZ6>=d9p!LA9&@C@jon5 z6WS>~lDv=oa&e_+m>U;VNh!*sdQ!Px5zQ;RL_|P*tn9+_;&AmXE8~Y&B*6J`m6Z1aq=eB^czL4T`IB&A}H@I(ZqK1Y%Hf%QtEOsXkv zZa7j?Mk@MS&!QpQ{rJ0}=XGnjQwDwswX5D!;QboSQ1|C*F!*hK8N)C`2EVSv$z z?dHcwIHXsfU;lzS8$zcbKAuxyL*kK@o+NA;pjv&1qy62uG7$OE5?h1F-umu)T-t(e zqxb#Ytpn|g1HW7n&+!Aj_`-OrN=#>ZLm_j;NzhZCf5i=qFkIQ6pr*-UH%`Sm{Cc!j z%67>d7Ww@m@S_FH6sy05N%sHSFZ4n--Q*hy>4Qx6zu7=GxKHi5wREqaOE^4N^XBXt zxu~>TIu+A>@OAC5_nJxKe~ioGrWuR&TAkN8sNbeJM(-7! zJi?@%T^L2c*GxvHwreqRe?BtQ-=jP7XDQ%;1yR{V2L^wzcR9jWVJ;0Nha8{X_;vVw z6x0i#4~=&8aCT=3S1UCDzcCr6RWmT4@oHDwWyU?E2gKy;xgsN%h8kwWEZ|Y)ek@Q` zPT7o=Oj1;X_yhhWd=imMNDHA|3k!w)V1>n+lKe)ajGLkNZ%S)ut61bn2+7QyFNtrS z_GF5RJHF{9Jt0Zs#eaI174gk3#ln#`G%vPkU|hei5PmKOn8a=$if8Cxv;z`&LY+^XNL?{ZPHHp-~@+JF$Rte1R)L z@J(aw>_z25;3w-i2KElEJQs3{p-8e{!gPNk&cdgla_3;hCzOwBoNAvswIwF)-PF+s z{$HgGypJ2?>j+wl1K?ZUgt42_k`MLJPrCnsD8chxTt4Md=5C$dF{Njj<^MbdbQ1yF z6zJ!FEl~dJjr-@V!PMdLUFSuU^Hvx3$5rR!X?}3tsoWs`F61|cb#`59^Qby9Z|hlw zsEhFiwL8#`T#QO*SU|u2Wfune4x&~^b@@v(tGlY6L^nGpKJu%|jcUudyUWL{V`;Es zFZ8DZIYl^>A2@F}=FoDMJ;eJ@^+@uWOHRMT<)5#a^9hfT-kt;V>(PcQi;Fs@_oH|v zN=s;Qf4qD7ySSn+DE^q_zaI8t~t?F-j<+ z$EL5pF}e-#Q4Q@XokGdl`1<$SkMKUYgbwA$ZHH{HjsAAuPNFv;@~drg*1z??|HTIF zt8BNWGls;|A{@^>82s+^ohho0&@ZUr)u`$t{KZM7oAV5m4NtJ&Zin;yO=L{V$!5vo z>J)2`9tZZrzW4UgqGVqW9;giMzhIr4c;}ZGo105Naququ{;2D;(?41iyk@bCvOPW4F#MRY%AVofhN8Z2+j zxwQ>E1K`gg>iRknPJd};WuGWC^fx-x;stt0F~_rRG7|7jKh=n?!P~HBcgvPAHq?Xl z95*hB%nMvy+oDRYG3%s|rzccPY+I7QoXukPHN16T;F6^Vz-U0J_ zV(QQ*b<0qFf4Y|)^81{6>aKd)74BanK~5)JNJ&#`XEwAWJYy2wV!ns*x!^8u7v%fK zNtnNWK(y=8GCa1?yaeeX;)cuz0@V)Gs(~Kp5yTr`{|;@sd=LBUZSN!-o&V47z8s)tC@IU~-qgPPd=T(s$5iCL z_Y}QS)mO|(|BS-6;AieE4g60Lc0s+h67cr{;Y>dGM_wW&nMbKZK7)E~|32ErM%X@3 zkp=yKg#WJ2d!TpwNvsi97S*#H*zce%^2};Kg1?#6);#gAUq)uG`9+P{sDyKk7L{!n zL`2TuxHRxhpvQ6z@?$1!;)$g)JQ4op#0EKNamg}!CwfZ}9-$WK%t=WtRvYTR$4BQ` zOsncDOWgPzKV@|)ig;PqdiE~b7YWF>4C-sb*kohl^Gj_Mt*>9cdb+#%D5!?-V)x zMhdvT&enHv3<7jM~DT>|GEf@&hHFMDO8VDrHp0cC_QU_NGCpneusdX zPvY_HlFJev4gnucM)QrfX6=7Q*Jzs%;e2-y-n5e)n(dYn8_xL@F841v+7@ks_E}Ma zuKNJqvvA=r2frI}&EJv58>4Py=v7;NL(iK^(_kz>-)BaXxltqh<+>kryH44jf%BK2 zxQJEJ=agK@hW8EeA$W33PtQ?iy`3OfQlG^x)Fiel!oE))F?4a>-i!kS@o0W3X!|j@ zZQ$QGbK9m8zU}Vak$?XV_obVYpIhX}?@di;jvG2?$R3U8mj>x4G_Y@ zx@=umvxPl+62EKEPpzj?muY-W#$DZp^tDKjMR8Y6c0qh58Svq$fs#^I>F%`?d!ass ze)0DKBF|#8-or5wI`7n-cE4G?JsN+%PV)&mACU!g%?+Ar#R=V;;rjsIjO(2~F7~Y0z_b3AmvR;L zqeQf;*C~94Y)gz1=t+tPFbeDGp@4Lg*)1!Nqj@QD;fBYml`?7>+9qd#kD10)H=cF( z=u#4}dm5u&Wh9$v{`>rO9t=cy?+vr0+=BT#;19@MyOu(H6i}UTt-H!{Eg~i+kD~g_ z#Iv4=*QG2rZ=JL`xH(}L#bE{Fm)YZnstRbHO@MWrwP_F@x;@~z^6>G>)Z+`hIM`Iy z`7Iu$mJqM|sg&=7jAES~e&e0|Fu*C_+{y^~*i#2c~@(3wTH@DhCzMM31E2odxzs65A zud$h~izBA#!afn$|I{9E*60cNbvU;zgLx?MN4+^xV+;6`<3l7n1PO9wb-Pkg{S!gw zmK(P1vg3GKKSK2}Vb)#9zxv^ZIQcZ%H`6ltjc3P-dlB*V>niCe?zu;n|Lvonb!J|N z_sQk7C)!%^nhy*;tP8li#r-+>?OMY;D*KAhX^-iTaW6OGjhT}oQ6!EhUYiv7zkRdq zn>8&~%}DxjE*sngMJv>P9(wvrQlG2iD3r*f_(?Yc=rO=ACqy{_C$o9IwSv}AHmMIA z?udUgFwzvt@R|S7cG(;zB7Gg?h`bj6v57Z>^n{}H=YjkK@pbm5w;WLYfvHyfA`y#U z?qu4jW$UB;1aB=!S}$L^N$;**vFL$yhVEU^!;2&7J_CQDeup^BLRPjTW#fFJ`Yg&< zCUl|$-{V&4Rmz{AVxA;6=Kpx|=TG2MG`QnIl6d5^%)FoS@?Ok_@&Ke~poZgVxvalp z%jbptwPSJQuGP)XWi!x^DDW|n+A$qhL1$vr4Y&Poj`(Z06yX2Z zqhY0>uelhOVX;mMeSVcMGy5!MS@Lx~;pu0Yx4m9LY?ZZNA4EBnrz@#fErUl#TjSHE z{J!-dJQ(3s4+C4DBgfq2q)$UB?ytv8J?J-$fxD zEuN2?q#r{2bjCnHHq$EpxDe`X0fDpGo)zE05mYT2&5&uc%){B{8$@1BqXH@WyDrszB_+oEA|t&#-Ul zdv+O}f_`N11=yd_uF&-E*ND8MyUro|Z)pE!?VW?jA4{u?aOZm+Eh@G9)^N5r z>i^6H=WaCUjZc;e>2T_lzw`93M|8KJ;?UZL!-v6R3HaSGexR0iU%O55*nEgbT+Yg~ zJ{FAR{G0iNSIECm^oA8jcd{t+KPj>wNk;qE=0&ye?^IYX)Ds%|?hp39W33ad%EF$Peoj- z=aPeVYsALwVj{kSQ^hRI4Po4vY4xH(eD5a2xd(2#>}66Rb-<=@X&Q&}uf99{uW7F? z(04U*aLwx*_!j%HFIK0T7{LFe1ZdK=?>KF(v4?!eo0y4K-(bO=vOblZD+~Cb9`nTy zoOKH+kX~vC_aF3ZUiDg4 zE=c{8D%(5RBM|dBaIW=@?WWzCs=m|0Ov!wbAWC7oOt#TzYT-z=wb$#mdMSMq%6DzxD zF6))1=h#Aom#JL*%U1bQCr;?j79##z6y5G1lU=riyn~_zd=JsX0r}gC#oq-*aqv94 zOnnEA5%Hf%lZRKs^8vjg#c7$a>Esc-hKewd}3{=su!Ka1J)3%XEGbO=pctv`W2 zWH@zGl|Smw)LfnI|2w-mKjlzk_Y;bzrHB18;IpvWQ%4deAwD*OzrWP=3!mc4H+5v8 z^B_vwg6sG*E2>c32z)Wj7uNGVK72TMwCBqc|7}Q*{LdvEg}>U^I`f>u^g_21!M1(# zW{0QeTKhVeqWo+U65On%ct7I@H+}uT{15%14(EdvQO!I~GVhbhI-j87k&M?Km-Du| z{QS=E^4HNk_>msRosjQ4BRwGo&onFeznZ+V_>39gU!K*t=->S#p{DM7eJxx4=D=?o z=4~mGes5SgPTxpvi}cD!wz<#yPUz2JXF_I+ik%C#z*tEKbHIO)3>rM9JLwgL%<6aO{8&4zQAT)~N^~Euf6)?sGs7P?fvQleEQhR02J%;_i72anx;t{|1dQYnvoS(^vw6oh6wRhH3uB~~8 z{>;vA=V@JbT|4w_O(FENrz2jo6twxxJ0Gcr*#Lg}7O_eb_G>k*tUbDdo?G_^K1+R+FR7p3 zz5X@ZSg;74=NX^a*$W-dwSI0rw@{-im7aSt?D|jO2bmgqlYgrbZ+@o1{u}J0U{3sb z0nFQjzIB-+!m}ii;Uc6Dth!oHqLg>gp6tF~1o*=;LpLoF>hA~=*?&384>LrjLiU+? z+OK4Kozc7iHFY8uAZDaE`Mbvwx5cCj-3w=EPHGSIPTVfqqRu1$u(9+5HCp z<{-R(h=1i~8re~X&krA}0ldtc61i|mJibzGHf1v|$-#PRMxyVGHo4C*4g`GOMa@?> z;9vOqTk0p)DCm_0*jpW(wtIovt*$?>BmZMJl&=AAvNnCK9(iOp$(bt{r#cgf2iCd^ z%(on{0y+fyW45rf`0|d+fXBNC#sj-IU9OXB;+Z8T*){*yZ)(5&u9FFPZ@;j{%Z58J zI@sf%acML9e9V`r#B^MylDKV|L(^%%KhAR6PLu;5We-1-3++dKfySWkn2GcW6rD$W zOjrdNGR+l_EzD}d!}A}WaWSyJ)W0V)YR~Qfz~93WBxx9^%)DY{G2tt@kD*5g=&AP0 z=ckXs`-T4S{@`()JvOqf!30Gfn@jWY|x%n{R^2X$Zwak5B%FZ?$N6MR+)`@nsMWW{5Nb4!>FHf zEc~o=-L3h}9k&txIQFzc2i^zxzr1ehR(O?AbH>QQwiwRK%>1M!s)uw1=~w(Ed~b&h zmrGlwubKHG0_nS{1@DPu%LN;)KkpDp^1au0o_?>_+l{?~tVQa^5gpeN}O_rH34_w90{ zN^2*<)zrWLaff}WMJ0% z$wev}!1oPvyyj|eTuUkQsZ5Zvh5N*INl?^c06v#)nMHUGt6V&ksj38SE;bq!|qz!t(qSQ zzY=!l(GO8Q&h+4kHsI$eAjC6CLVMDgJ30-`2(Qfe;!_ghx!>8Rh=516Iyrf`wmJUF ztmajW6u`%I*h7+oLu-T6^*5n0&_8VI$K2ATKVH1}`O{6iZ4y5mCf(w(P1#4XTJ>~Y zrrrIa74D#yj@-!Q86iJhQJB#Fu~g?hBhi-wh)-f)4QQ<>yJz^rlXu6eC$}P-r71a| zcCF6WCBHB1bD`01?-Mnt1HTS_J0bOqi<-Uid*>SAeE=TKW62biHQz022#5Q@n~3mr z33Fg7BuP1pxY3fHqkDw9hwMkwr4f4Gfy z4Dy?JUg03&?%q2)oLimGeTDjk`X~in$bn;bY7`Wd%NalE}ba| zXUi|js4grn)Ixr{KH~wd=>AdP%F%7dS8_(wFjkE0{ql7x>7}2<<09R`-fNxfui3fV zt#A~JVPD>yhXH*4B%5e3(7bHVyPIa;OpxA=vxP(fy+~SGdV&z1S3f-8w!>}zkS`QD zEKj;^Ir(osYJX+zSoRU_lQ6=yd7wc_#W29I)i7KCBhK8nE+ZoUwn*+4ot+v-Oaz-lz>g8? zvqp?cp=RS+WFkQ?V+8Xo*5#>1%4UTYS1G6-z$)eFjuC5bj4tsgAMvjSzs^lQrZR^K zCb}EFo0dZT;L1$IiN0)3c&@`l@es?)x90~n#xc7x`KZ3cNY><+j7Lg+Zte{T-*H|C z2aJ{WStSx?(EXic$1oS+K6_Nmw+OG4#GkkCVP4Q$t{H2%iTDRjFv81>gziVhkX0d` z>(UrV4;A$5gKDQi5Yes8k&pOvx#w&Q=GnQJx(tJVejRtVBK{=8%QZnijpor`tS$f9 zhC}n37d9RjN@K~2XTmrCYURG!WHqI#dLK~qrX z85$=qRFUYdu~#@Pa_cuIk$IU!zsP>=R4GH3S>qr6hZf+4MoyY^9hKsib@Ekb5_9E#VNi*$QsS?WGuSje_F;C#7AOG!T`e#BN+ zE=2d+INW;lAjQJt&fq>Bq-PN=9oxj`9M&C@(TRcgGt9XwZnx+Dbv3;o`Qrlr?4?}h z7`gh!5AM4nh_CeoKN^ng7nJe%Oj=5S*S=F9El*v5{*d{zXN1FuuZ^G!@rv=(DdisZ zSfuZn3Eg1zt4P%>&)sq1bYZe(R|(92h-Qp!?3!;(@Acg4*4D8^I6PTZu*VMcLEjUbox@eyJe1A|g7d@0u8{UHDO=TbKX0k-?2IdYh^O%op3ZA&b-?-ddwRX0 zT=+SRTRdxt>T^a17F}P*BhS;Xte9e>em%iUrmd|zJuB^2kPoZ_`#v3QhNOYr?E+!Y-2PpUO==Vl6mv;YQJwo{XWnK zYsuVQ7-SU>_Z9Zvrg6L5=`U(p^4~0!+^oU7)`R?prsul4ehSV0tZh1P7nT{QQ z>TNg_0QJ|~$tvHWXBvz(wZxuMJbZq?DDTq6qwDS|8XlWC{%^jh4-(cl?MU4+C^B5D zaUsV$5&QrqwqpxEV&*F9oI+0VDNo&7}z{$&_*rRCthTa|7OAxiLPgl}_qU!T1P zo05#`H|SSrN?l^DS^MF6T3VEHM62ckcMSdq{dK|NC|>-yvCF^lTZTj-Nci%U_a9QP ztXlJBg|RN+g-&X!PWvH`l`1!rQjbgYQ{nh)s*b?_V-q3B9|P(M=m&GvW8K9@Nw@!} z$5g)iOlfQa zz6AFB=4z2ak977}&8+vLTYK&l55PPc7V&tnU0=e-b=23<7|XhC%D-`vY+e4#Hy^a_ z=vU4!oK7yy*U^OY1^e9tr&Q6tgD}^#4!X=m+bTn&B}sT5(zB**da3 z(i=R}o|2(&46)w^d_47p$r^s89{)d%I zJC1Stumvd_&O6LYx4+-ENLf|kT}F=$(z|1x^p;-+cXek^HYB5XWx{tF#9!=QewX{? z$d0}4d))aFJwT26Swdox;{W_d_8Q(?!=8coJWVhnIRGF3JAu9ZwAD20Y0+t}-%m_< z{yCTDht97_xB*G*oW|lZ>@6kztKj-}v7OHi;Vp4_s0Njj?x0a57S2{jmW~BU^rAle ziX!dKiJlLJlKEw0SA#?KXP22?e6bno`_{>@awo>ae3|H(%_oupFTW)i+1Q;8+*6S{ zj8(#U2fteB+3f!J8n5=4eQDB#?^h^fF8yU5o$$wUK}>(T{+*&YhhE^*8+m1x=>Bnt zOe;!I&X}_f`z)$wF>ic!cJ@t~>Pfsm@;Bq8DXALHf+m+HqU87rD|5+)(u4iM{&9{5oz#AVktje`>9%3A=A!i0Va?v`0qz`u$&%lds4EKKdsjT1HAX>bH#1wf?_%TE*_oX#Ex_ zo;m+3Ocbdr(J!UvwBd^n6&N2rt1FpDNyX7g;xyg%;|X;8 z^v+nT>{xKYwWl<@wjKOC82gO3m8v?;pP|<+jVgs}YgkNEv$x+OO{a0P0tWZ=RvosN&I5k$# z_)}@nFEq1gJ?%Dd9t9YkDK+MNBJ1XJs}k=Wk!E)cRvd@>K1VA zeXRDaj=io6Wmg%^LVN>$Hs8>LAbVuK+Y;ej^~efe%GbEb;i;!rpO3-$KQJi{rv&== z>~~Go-4Evz?b9%{=aHr5%s;>?jn1fwFK0Cwz}J~I3-}b^2Y0zRLO&7eldV2tIfxHX ze_kc=`8S7?*HjVVKKEl`qt74Ab6Gp%AZW=|2F!FK@!6I*7Na)n=o#LKO7-!GAmt_SHupmK({AY{@qI z8zW$fhZ9BUyk%*;dIIt>1|p2?H1CbV}_G0`ugTe zyh(5R;P3ZS)08T7rW7w5pMm~a8Q|CGQd^4$kqa`Lv;g11ynu<&uT@{JxbrA45#>WH zxZcjLnUsC@c%*|wFS_rP!4)kPt%=rpAJwcnN?O0?-0*Pxp7k~|&_AgBM)2Oveei(W zjmIVdKSTYV|6V&aRiV2i-v#&p*l)PMlwnJZrV}%yMr<8`Pr5o*hf6qaJ-0_S7QU}A zB5c0Cp`pBW&xZOK-TkZG&cE_7o<|9DjR?R2{Z*MTAD zdYTIOcbJo=dBd>n%0I_Xp9@K%SWo?{hj7~bJifLF;->)g+{9M9%j(HR!<@q3{Rst) zSIMwXippI#bx4wrV%(WmQk!hW)$|HuWyK1penzFMUo$bbD9rTx(3>p%Zk{_DklsK=%WCs-eH?VDG(blq!)KXVCw zLNa;FqlMMn)lUkg?z8VU#s&^x)8YJko-0tl1@i*@$$ZHo>hI6kxnWg$VfV#d0@0<@ z+YMh%MqRt*UsIhb;mamtK5qj*`e8!()!8|$Ri3#L`Jq5RKdiUJ*5v?a?!zAy_k-sKb%Kl#LgiMr;*IOcS-XuJWoZK9KI^^?lAS+ zmxm8gzQbafbmet*`%d1eJ0O`C4Qr?icYm#%@OeVX7QGK&VW5Mhb@KY$1Bl;+d}v^( zPcLOxBuq-nYe(V^+af>Csk!aOU1?~41>yB7TrS1D6L!H4Y$bXHKropT_e%`6#W_kA_q}Sk76Uo+Z$+iM!;~$}W{dZ8>RrS$) z-HjhTOG~g#byz-*%NyKVQBm&4LHU*HLrKxmd2@G^?UMohTeNRdo8Nm!m1$c8^#bUD z)3-Rd_8p$s;Pkj!GQYIr(x*=s59nKXo>6W}0)C*1Ig86_+N|Jp1{bsRReoPV0go5ci?M4njB-U;~h9a_Xnrkxa@b>dVD!WVwU z;Lnpu(J+TG!hk1Sus$XBOJSRL3`Js#I`@NlP^I>ne z@wrUY|E4}EE=e?Mc~Ww(D+uA+@VJ8AyMvlj77%a1_Xqtm>E$qQ{fkv+u;+in{X}|J zp3OR?Ejnx;=%+w`>ReRGyaFY4qF^{#=A*z9x6F5TcBFi=ROk=*+ZguSjq@qh>WrBJ zlZ?@S>ZY+~v~P7zl9b%sLFM(|-7@7*)s9T8vwK&%f8}Ug75JNARLrae*W}LbhJJ`( zdptt{=XK@*dFwGU${#F$Th?{`-z^F1}_U6m%_3Gj2( zz*7Xl4-Yat;XJZ?*k5T}dR%{JSmZh_37(@_NI$cbeLe&HT;Tdnw=aAO)<67$x=Fta z_%D1nwzuK%>NFc{sUnH$*X#1HPS2UBfa8eWHI>ENaE-DGn;Xg5Bev5d7qNJKj6du zwB+8HR{W-i_-3{mlQTm}xG`M@dxG16uP>xo+vJ2V4I!H3gT8I23prI%@88(%w@Nb4 zrft+QMDDI@${+5P^xJ(46%9-F=^wsTruQE{w%dBWP)%;Ick z8*bUY9k<&bqyT>765NS4Hhli}yK`zTO5)!&ecpjJg{JpUS{_01N94){r?#(F1HY@_ ze8WDPYq2^h`jBmwR|~HQ+_)cf^Z_hQ7b5`_Wq` z>1T-zH5l(4o2}Og%j+cft?wera%R&7tag3~jf_BXJ z-j(zKz?TES-<#9%D&^WItnP2rwB5aA|GC@H4;1%2G0=~hj^h>U`|$*Do-F=#eYp3) z-6L_E0cD$;JS!3?2iY0t>ky0hW@SQf}mKpGi)kB1kLfyWOx_h&2&K7W9zJ-!>VBc8TgYeJY zm5&FLAAjCVjE8-|7r4YB56}ZJ!GGt|PpDVlCfe_We}9l_NppV?*G<_mDZ*dGJ zb4`VO<_DxV1OKB8MoIqghV6;H%6C9-JQD72ij$GDo+cUZ4ng{$@Hon&1q~mP=WE$( zqvs9#Wo5W7HVcbl^`4@9M)0dW_*-}NBBP$=La3MZ?=rW+{M2OR24aC2@fB1OWsAZ7 zJGhQ#@ij@d|G&?tt0>`U?>Nj?{XZXCZZ4hG1N__wn^@3M)p2EBj7HM{oab-U;{^eN ziJUoP(Pw>Y*tbE`A0i<MUdg@?&Ez%dWq8n`ZTb2}DA?cS$@?(16g7rBEZ$z3r)CT+u{np&#_APNq z4}ZUwNAbnPw_hVix;Sf^ztc~m7jcoPXngy#m!Fv+mYbXR@HqH4z^dBcFrb7G$ zJX)xMZ(pLJ69f7yz@K8DU-au&_UZk2W@K5f_ILHuNsMZG%lB2|A54C<9R7Y*Uc0Ky zf-ApkzXe>3h#&u3-U~zj;oNwq%Ami=4m&f* zU-izN_02)goBaURv2w3IE$RQ>T<;er*>69$K*~kEAn=dmeCgzjI#aGP{XEs>QGX0e zktuO(U!M7JgJUbqGNtHPSs zZ!W{8?L^Kqxql$vPllTWhqhX8n6xV}_fInlAbL2dRigWym|cTqSAM{vv;f!qK1>|xpYPQyd0&<=?R}z^)5-J-KW`XO=L`C9_;z(v&Yvo{PoV#V zeUN?GH)e8dlhOTR7hh##v&@|~6J=W^eAW4()^@!OrcHBfpnd_oafexwXmH*4xNqlv z#J90IO~dV+y>CYQ+)v9*WxOcSNAtpro>nd2%l zoTazjxTjmj;rx~z@SIJ(cF+bOuX>?NS@Gb#Z-;CFbiK9}6rudTH!YGgJ=~x)~cC&8&}*HV|VWKL!k9YoF(o z{1!79{ln)M-m3Cf9*h%;C!R_lnl>{(ig!i)u(Y_vJwl5Dd^7Z?d}&S;fg8Rv(-!&( zcy28&m||ettJcDauPrJ%YjKa{^1BZpV~I}fUT(#edF+0PKYs|bvtRYs zqZPfIEjSe?2_DT;21qYISZA~V@P)yb((n`pZe#F6@BEo9hlkF589KaUWaQ}LKl`Pi z9;Lq~_&Nvj`B%**L&8~Wj-=##N-d^lP1JpOQLPB|0`$X7z`lmDx+a-jp@izyh!9}w zo6R-+?skoVp7(D``(DV8BGw5HH3hU^m3l2&Zrc=aWNp^tQ&Vv-b4p$Bq4`^y`@kcZ z=Nbur5G5r=Q_P+FDZ~Z+K9jxLTAPdyKG!@jI^G5Pq9Z}a6_6g$@64tm;BN*9cw0ro zK69gha~{>u-ydO9H0ZcTOTMvItn`NdMFWP@qr9Ax6MJtt%STd=4C&=?+~#jqw-H{y zXntYquK9hyF<@9J-Z)Q!H?Af+`nM0`8#7LZqw^c_7^ZP)`!qHdPPLn_w^jH9`xSW? zX534|!sZtlM9>CXzR}R(t5+GJ9<8_d`D^L|H*uRV_)gZ>kFUlM#%gimd(qN>5mYVdqz+KO7u&CC`XO zgv%d7si@wqjs44WiR>XmA~l@VG6eo+>0|uL+>(UpZ1yztJK*o{dchmEh&rD4dK&2g z*?S8O_4+2u%!Ol`!|zTHT(p;-bF3;{8^B2m(ice&d#nt{oPN8J8gJ6e_68ea9^6h zuRg%v(|DcBlA9e+k4=i*#6^#HJdeF=PiIK*?FJ+3iN58E<)Jn0C|-#&f^BqiRLpY7 z6_yaMpg+RwSFXrjTb?8rgZwdTq804N{7jbysi*#9Uj~1EHS$SZ_5shJwF~!bM)fGJ zucG97$m^gT^Hb6Lz|{QF{B+Clg9FnNKEZhxL$C7R+8?(b+roVqAy^69i}6;6=Jy9? zX19S~!rRo)ndj^CYToltnSWK-f&2}9ZymxWvR?V2@5|mxD{+eOW6jV{iyklWad2^5 z3i)oj=GFVN$0hOCYJ7}`zx1zVok-$$#X1*eO{-AX8QVqp1Mv)=N41Wlpi-gZOF@fd z-!S+o=V~})JtonM5Z{DN^T`F#$5-<$sY6hYz7yo3asz$%2;x+=gcC=rz2FWC|aOr(drpmQ&<_`6DmiE z7huw+f3q!DNrLyT3Qtf{HS%J1-M^k)+K={m=m;k!#{P10PK}w0w_`Sv)6i?WpwnTC;xdm6+2l!^cfaUhL0h3wO#9Y1I7R6UWA*tQfp8Lc} zZ9;p^L3w}IB8S6&m z<$((nHzk@zy)@__xnjyM44d*y=yt`X;C>2BXlES8s9xE`T+dr_ssV2fQ_wu982ri6 ze!bGS5&j=8@0d~J`V@}pfd8e1OKXax-l+=e-G=Z7HN}w}RN40UVLsGPz{e0WBFv() zWv?Xtj>t5BKXDDsCm(v{du!{2ZzHN7P2$dwDeHffDgI~yy#crE&1cj)Qvo{wET~um8@sto(5EmDH zWz%osymH!v`-tbWf0NT3H7E z=Io_Ci(<0nWqyl?vaAR1KPehH4*fRZ-+3qC_W_^l!BD8GcntM)P12Mm+MnZs^oF&& z#(K;~+bEM5`?9*Ha{peOOf4JTS5ul_`vrZgozD?t>8Ec2Ukm#wd)U>qjXK=eJ*PCgA>V_a)Q$pOTfyP^ zLEDXk(D`G>IuM`PhRQ#QeQ+J|LE@MK?q-F#OFf4_?*%`BhK-vZ<7Z)CYTiZdNb|KP zV$gk`G|HN{bB@uRWmDz*^7PNE_KT)3oc=ycMDf%lq`yKpFBQzL3U4YF;1U5Vzy=%&C3_qsSaeMj#H z@M2{}%v}BVQ46|IJymnH^G-=g-2Jm@vgcqPeE{R?tb48JQ+zY!_u*xO(X9^xmZ2&p zys(%ag6d1^6TzlZgDBtQE?@oO{B^TIp3`)>CI0C$TmP1Q@b^QaE1X@K>O9$=k!Y|x<*xcVt+P5yn+G5^_@zz z1_nLCS5Z>$g%ra4*Gpz2x8%pvk2qHL=1`b77tcyx%ngO}BQ(7RdZkC+n-UkNXrlbe zPM5W+zV7tN(g^Ke0{@xwA85m+S4|p8%=)RWWMXEit3==1z;yt;3Hw3(wQSm-s;=(t zs^ymjWapd5q==@Q4Fr00PFXiv$*gTYc-xU3R-AebY%D)uhXF?C*M5}|8l;P4hq)ioN zNUs}WH#$ytVgH=P6pBV~KXq<(zl-({HQo*00q5y$EhSL=78`l5Vn_zQ68x7#WL&6} zVfnhQRL{{9Po}A$mXDxzr6nxKkB{Ew!afVpPPMsFv0>Al@O@r<1;3=3jZ$%XPcQ5I zhQB|A`33A|7&e)7?Av!~jmP7hb=z|>_C$TwA8x8Cir+PHtp}={a=4CA0>k+gguB^x zcbPBL2^{zh@eurzC<+x-8pf1kYdb+tNF5chzZG=ibJRPM94@sm0AJD=%F2|NuGb6g z=OhKa{GVQU(3V}vOWBO(*N)lIwor*{gO{f#lmWj9^CW)jl?+z*%doxS-v_<|SbLP8IH< z@w>;~l74eE(Tn6udJIrT3x_-OxN`rDRst4K8 z`ecV8?uv(fVUIRLehAG-&lh;CyEXs0Y4s5$GW>3~G@BN!CNQF5x76>?q|=VCHZWj5R-%6l8RyU#rG(jAGV119cFU1pXZGs`tocIN2K4>Iea3g| zxqQ(olPM9%2{DES!6`@h&GOu@TkJqI8CW%qNjLKC>tt z@xkH#h6ZQFAGnNnodLe@LoOYeyvb%y?mCgL{y`eyA9j&mYgfk}mEeW{%ERvyvf}i3 zFRY!P4(`@_Ea8WYda^b61B`cQv`wi#Em2W+K`TS*1^Yj=v z55n+kA#IN?{#ftFF&jYmfaQy`SZZc1qyDDHxXU2G#>2_50_JVlpmKuqM(-x1(2D7d z*j>q$eZ4IY;q&j6iTZ;V8qG0!9}u%7xXHWaD)RTQ)0LV+{P>7z7x>%FOS)}+CFD5V zzixJ?hEp*A9kbQlSWSXQNuEIrB}`>@*M6pb66zo2lpE+5hFa+~Yzz1ee zm_d9Nuyzn1RT;G4>kjaW0e>StoMdCn+nBICehj`J7xTB=Z8PyOJ=MhO^y8_xC;6p* zz@JV>4K^wb_ck*0Zgx0|Z)EdLW$8|cNrjh@~ygZzP|axJc$Y^xHanFUY= z$P@ug5!C3*JSert&4RwD^Ph?4`L#bkRh^pRB)|ArhW5>gBAtj6p-GNS|J#?c>xyoA zA?WE}{%?P9mr?4b*S3=T=qgJvpdH*ac2OZ4`De1V=K)?C&~I9%)Q0#9@DnI^9J@5v zZynK>?`+f(5R$*+q`&Ucb5Hiz_*3Y z!R>LJr)Lbu=An6z@R;qoC2Aq#(&U9w*_)AGf!r{})6*NaccdYHQ@r<-EnRwddgG$H zi<@EI#iUR>o<>_++ImDO!_M>VrNJe+bt72m_EwAc1KTvuv78^hJuy8tRddC{Qx)=Q zm+1qNb+OJ-yW|ZMS#UomSt)TFcU!GB*7q1?K)jLg5vI*Yn$B>69y-^;fJ|v4hFu6gH{Zxt z+jlV9EplYyvU3FXb57T)V28I3(OZH46wRp*YDK@lW=Fw-gM6dPGjYuKX#NZ4Z)X)1 zWXe!K5XG<1xJ&(XLDrV5m)3M~=K5H!-oFI(UuGS>Ds^R!h9!6&51wrnU0xgh!RLj< zA1s!ku^}~0FE~|r4EiG&)H@s!?}dKTXWg06c*OsLpVNf){;awWR%L+4LfxB`;uZe< zR$KYN58nTPm?RcWXQ$-OHQ$S6+xJ4fdn;PUCb-=^dxnMh9D+`O4fi2gZ`vXs_#n`K zr{r2w8JhTn`ivjw=Wurm{?m@z@wMuvagy^E{RXe2lVu*L6$S< z!T;ZC()1WpJ&)(HHoa$y4D?H(9?8|YY)!ry>_!K^uUS~Tw=<}6-W~OVhE3rJZ%q3s zF^(Q>wR_mEtq1WE;!*H!rT8NHRNtvm^gc~(UVb_eaJsbCwOV;uR|c8Jbc1XaO{&x6{o=)&5KMI;EHG+1OEo(ixuoqzTx+` zv~}p$&MIDndbAh5AGf^0FP+X{QX!rL-f)kwQBqb%Til>l+VuvezDF*)Cc#A-HpY3k-1bn(A?^=P<>D{f_bE)5HB1N)s0QK z0v?3@XCX8oP{~T)zU&Z473?j?N zX($x@qi}vOnORs`4Do#OLcv6Y8JgE)g^4Lm3ge@=f^sdCKgFF)`-!VnS19JyP2hL5 z+o9tN)RVBLZp=U*?icu%cUB!dm{w6qAD)pL%qXWZhVUrAJo67c0P$jk;2O8FzKzRl zT+F?L@I&nmJsK0ctoQhNCge|;-`yKW>xf*wQt(%46FP6ybA{^*7z1SqpE_p&4>pMX zD-R#m39bxSWphQs2cGMs8dg;hSi>^JWd^}K?{>A{z>hu^d%?UQ4)j`8;J3_Qp)$U1 zQ~BtrD)2{JA8vT@VBc2RMSe2J&)fPshs^#OY5%09DWlP%1o1Fch6V=9nJGE)_ko{( zLMui337DVfeLnztoo05ycsrT*Zf~;CRMtKi^n_PQLp;3TQy}l|MSF;+Y~N0&_Trnb z0=y$|=>A4r({H%2Zb^Q&!kYJh@4mx6Hz$gx>q;T){X_VQY7^X)i}PFB>Cyaisw>(y z{imxb+D8zzk!M^4^ZU7*hG4k3k8=rSs;-MA*e z>phqb!*Rj___jk2u6Iqvy~r=!-iZ2_F&%;&``d7Sc4%3UJ#WmDTV^#N!DDyq2BI!L z-}d8OT~62NS)ym#k_yl}&uR>qCDy+yF9fa@TNiw;@wL~aR?^M?pI6(LEJe~7>(Y!M zy0uxN_{l2e^eIdFr|xz$+vFQYon%D^^Zs!+ z9~$|N>On%7EYrC%%E%|+>oF8xB0>vzHe~5!@egib_M^{OGiF6267h8D;R19&zf&vC zljN>g$O$EUjNh}iirG|SmtSY) zp+5YPjxBw{pmj(KLv@yQfnQ|qrj6)+MaJSWsFzB+#GrS|nzKjmM?;aG{TaBQnMw*J zJJ-|@FYX;Mf4^lL+LyGB!u{j;QU>rL%v*$a+fPKm<A|_6jlBHKYwkr_KBg3=)Q}#%jOh~Dw_S~v9j!gpuWCpmq(|)^AsvZLp+3hpF$!Q zN0Sz*6C+sKU%vVI4f=pTBISMmkIfSZ$qi9(I!aagJ8R^}E9nUfcuq zC)CTKV{MGMmc5(4efEL=G0d~^tBl62H($ZO^^n{*8h^{GB^F`5UrbV<-t8g;OY!<)mG@roPQmnZE1(_LMX+Kf8fTP|5i4Tz0#2WoyHlIiy1WGR zK&B$EZ`+`r>LNVY#VOXYGD{A4n}zgw)M=u~RF@t){EX`khg-hCCm=pY0*elmXf6`Yj5?Am~4g5d8f% zUXA@DD=M!EW@q3@5W1YpV&9ai` znVNY0_fGHlRtKAaq~!mfU-);U@MII@f0!SXE#z_^y`}|J4r8bv61w;6x~&2glP?}S zb5MUDGsJdz-YS&-UXI5Kq-Tm>VG)^krly}fAgb#DlTcqZu6hc83hzMMB$ z^|#wTp;%0K)-w7B;h%`_vbfu3Ydr@v<*nzvFD^XfA357RA+@UBB5G&R@7uMmrQIiB z-jelP@YMsIZxK<+jNkT^H+C8HD`?)dSdT|`V0+j}8`vTJ(e`NnJOkVIZ68)X^+9@W z4E!zHTNRVMy~81Y6Tttw(@1&l?_k9Z1y@>7J$ft`=8dagtbF=maT306=)e6q3s(#2 zO;1q%rN+IaKdMNKc(ve_p82YWaxdXs8&6h?s#VU-->BuOe~=tfBK$V-lwGBI3f<38 zg@Qbt$^(VgEo}o`gSmB|%Z3_IKG6Zs0dzh>{c0aKyjR&Qbs$89@Wyr%J@SBoQ`AZE z&*UymA6p9LszhIP?R)o=|NKW>+?2OmxFAfrb0?{cEBMdv`-kJa+dKNf|7o{NJJegS zztg|33MaQ9z9mmL?H1w(uEZ;-N%#d6|BK?ak6Q{4N%RlCr-+J=q|0?a?GeI!7JUCV zX-sH{opKZmrq*6 zP2g<1W|kU4^alO-)6egF`v(W(tX3-+Il(@F+GwxJIKU^I2vY5!Xv9!HH7)Zgnol5T z?{aW!jWw#$4Qo)Yrt?yAENknS;}^5^yhc)B)Bp4^PTBdbtaF?{$4zo>1TUT$`-64AGD7=zLC->Z z^vGjz)ZB|zwdj1*q_oxJ+LwdB|J{FqZ)D|e!ZjhhsI#6IDtSK{K1W-tR14;gfqv;0 z#6N$__9|vnRDVcsfTSM~yN=TLXD+^_UN;B!y9{B5pvM<@CI}o7xeoEG6$5^Q{gzr$onBTf@AWMzDE^qb6CXuVMh2=k zOeg_9`Cq;4tkyUla0}{d@P{GVP~K)epIgm4gMMGAdjjp~QE7!A9@P#lNl-r;1U}QF zE-h0QRo9uXhb9an+Z%B;Srd}q*;qB@MTB3$>v9S zNbiW*yS!|i340kf*cCH9HO^2I3kU$s-Y5D_r8)-Zi8!qbDBC3rg0VeE`V z4{MUabuuz&sb!6H5&@5Oi`{PjRxEKT#8nH%2-^pRK%?`=6wlK(bnVRtw~mKHGw#KBikl~thr{y;>z7`GGW-&K&q7TcR$h5B)-a7F<@n zTCr%L9P;}h+ZRVSI{TpdNslrE@^KF}L|;KGw!uVBHGIzLaG=9r8M!hU$G1R{=rKA;~_Wpp?YCEF}`23i^S5*bXk4b`y-Fn#@ zK3Tsvxc#S3OL62aa9$^0aVen|_1m%Zgo7@5Yj6HFVYS$PBjEj)ZS7?8fa`!`l68?m zz)R1zE4RLUi>S*Jc^-hz|CaiI#IqnS-sM;=9)S4Pjo~WGaj-+ z^~RRpsD%4E$PUfP9xUEoeak2m@Q>9bi}sUF&skgN<7EEd{8_X<#Va{qW#*9El``l@ znWj6K>gi3p?03%B(YUwm{_1Th7<+O&>s#o|2H-!05$^mg2F|_mDWAGxQNK1cWG^ED zzl-sCv5_p~Gw}CdI#L&XCCsSbg69o-OMk!i>$vWJRYtVo`NIB_XklY^@$O~)#za2} zo-xX;aCUJ}5*}JB0Q^2;a?a8IANvIhwOSufgTJD_A9gHS2j6~Sz;gv&(qDdU$IyM} zty0%z<$(Ih?38&lN9<^VAcxQi_-;___hJ6L$dLKrT`T!0{$USJaf*x8tnIv~si@v% zWduK>sVVQ)pHBK4_SG~~`OZO)a#z_@nYBxy_*)y}>7*cae~;`hVbx~1AL3BGqFHuB z?Ah$tJJ#E%Rhs0<9W!IWbr)$a3X**{I3=9pxRbdZ^=%lc*G0!=U-<04Y}XS&bOgSj zi|T@FZ(mQ`+lxhPLi~P2dU1XBp-t z2n9bcTK|0`KbwC}qxp^PHjLa+%F?PB?@9RkuwSu|reo8tJvm@*0G}83>$=yra9+&W zETt_D;;f3WSbdgr8Tkk}NNcw))JvZuHhj!l zd+~+MaDJd4XK6zYO1QG1hc=Dkcc{CpwHGCI zZ=%nR4^Y1~Q#}kLgDC?SgOA|b1_z^T-x_9i^i7Dv8>$npcd7cAkttu`zG1cNgADbd zpW8(})_})9k}(gg93DV=M9fWAt%Bt8SzGwt_&D&tsOA3MbHL@VO|fzL>ZqP)-(lJlUtH_- z)_b)-?V!(-?=1$`ksp=bu3t}*+Wz0~L(WQc$|gCx_7nKYE!EeeVc3Qdiuq=AUax4p z^`iLr+0DLREcf{M?z{2E`F$d6_DJu+HNam1UX6;w(Yg6I^Vm?&!1=Rjs{e^VNTGZ#*Z9lK9D#R8;_z*jr+wlEIOw~RR1{y@lFt)qEF-n4Ts}0We%bGm`Y42A+=`8r<&9R z+9v{DbkSIE({3YkGoxu7^_xXLora~l&+nHMSCNqZi4`;bj+FE6tCp8N==VTRRPb?# zhAXK!vKskovbxw$d|nnXte)$>yYOb4^tBxa_YZ>pQjF!C#8hw|Q#Weai!y!_Fz z@RjvC*Qe1w_Ic?^&C}`4ccEX=jKvk4GptsdymV~p8hn4yKMBveKaFV%`6(;~y)x*> z+FE%$o6W@h&lu{j0sf;`^_%As*LTlM0sha|`%XvwxtxvdRLS!oJ}h&5`#D9*egNVV z!~?hPmgkpGv+mcw`cKcECVkNB(wD~xD@jAB|BeMS9R-(`DNSs(6@lJ!gm8`d$GC`d zzl8|;Pq<&co{k9#k=dG{qO8-6Sf#zjjh4)XI=hz{A3#P86UeH`}j#7Wr z&SiW6!=xPlgi+vsg!+4g>dZ87?*E;6vODg&y*!*B!#AxC4lf@ER)+V=&5fwjwm)VA z4s3Pz%w8Nu_lafb9Q@+ax@+kTW|!=d9@`o4qRvfC%U*9b@IzDBIlJA^i&&_iv$XE9 ztQ-IEBK|7ju$Xma&u_naFWq$X?@Le3lqP6VG~js-Qd1jX|DAlU9`#snnihrP)%Lm_ z_?}wwj%A*md0!6~ECp!}HsPzX;!qp>{;miT<3S#soB6oC@scFo`P%U@-4t_Ka@QBY zBZ6r+Pd&Oq#XtJr+K7^P)6iPNT(yjt)Ng$Q&9g*g=;6j5#blHe_nw3JJ6XH;xrMRW zvdODL_X zJQakV)8J1mS@7Pjm-ZX{I7Vt$OvA!!dv!mbh-$P?1M|0#l2S9bP9AZcL4HygQRxq# z=UrTLh#>&H0Q14pRnB;B$wPX#8LF?s-JaD}IlVWjQ?nh~GU(X|*m0RLzd7w=V>1&?4R-2iP1~3u-Zyv71^ki3; z9S1+bro-)&v!G9^y?CKg0r|O`S~17W_ffBB?;6;=On zGp-5nc;sbs{)c-1pKeHBi?^3?RHw}}qy9MN&X7qt_!pz)^S>qtZ-5?yrh8fT${4|> z9K}!U&Kvb&qO6SiI@lKv^TO<_^BSBYmMf0Ci2$F0eyKa6)!MkUX@+fb1>%EHM3ja7 z>?$pWf^?eQ1O81vS$xxSi#@M!!3`l}I4 zP~gaa>vpfxUrz}56~p#A!h!-zgLxBWo(TUDt|`+hIu!u8Jph5^XR(fcfK{p4 zbO`k;ME(q!>`gec(A}wtk4P*4h zm~JkE{&=X4Y?I$-Dd0S=tFJJYPSAicl6&4InP;zktx`h>=TOb~PW098I z+y8Tg@grIP;Zo;1dOkQblc49u#(^8|E9W#FES=|&W^n)G^5nPPi zow%#HGn-aR)u)wP-b4MlhDNMGw?JlaXN2WjCp9OSUtoRj)mK3JQI_kv)J3GPE1II1 z|AJlx_OGt+oyA~z;a_X{Xx@bV_>#s6{8Xx$PU=ti{aox7_;279wTYUOStx%}JDE;> zi;k-ZJ$@QZ^@#zraXo;~*E zrqgDm7r2^BD{1&KO9hXnM`lf3?=G0T8S)*w zK-c$`%=Fbm>O^~8l>Zz(Jo{QWRh86R8dhsm}bDNLv@v-ux7#e&T0K67d9R<(388e6^zF;ifvun8CeKXQO zQDi#)9C$#;`0;Ep!ta=2Lu)&Exp6tjobSU-H}A!bw5x zm(Y=Jm}eUa4{Mc`wm*omR$kjPYRiOqP#EYjY_YELZq9;w@ORA)pQC>GJ6AT^zlZ)o zvtYXF`#+A^{VCq&Re*0c^hpO1xneh>-% zd2<%3|D1;32lFnLb_M&52aM#Gbg(7yFYG0eULhkdAJN_O`1kF~{!;mhw$Y8IGs%b>qREs~XyiQZMwT2(q1z7Nc|YSAkha}7)J3#lM$y~UXOL5*iJD)*q@5uc7difEXF?@-PE z{26{OfBW`9ddRvtHy(U0#HYc28gi=y&$D_f^a0O9zt4CJUAO;WeZWbb8PtC^b!V{6 zXo;_+7d652+bs|k=YU^$QvUsS8v`WqvA~()ai%*h;EvF@Wih<}Lg!(gZp$JA3)<&V zGN>GGc&W{mNi#m3^@<4kl&6KIM7j_gyAZqazkSo;mBI+)?3SpQzu2Sbeuk4M#+AD6 z@58HlJJERx&+rSTxOi1BGP$%L@fi_5Oj?CmqVJxi;IE|&-+yVY4&D00unR-ngyNs6 z>n=rjKWt6+g;J0|gyJXSsUh0#)eP?`66CKb@t)jLZ1(6M2fQ3Kh?LKA?Ptxg@2aJ$yez_ct6}< zc6U&_vwlZwd*_i*U(OS3PkqC)Rk}WJ4=Lq0(vP#;WNB4(e~Pn) zbw)X=;0NFK1t**o)eUK$fP6*}m=xQNha8Z3KHHlj)zSp{#$8sqqTyKnXA{jitd$O5s4yr#r|8T{~c{r&Yke#z9@D$W*34jmWEUGKE*d4Jj+qD(&PBV6O*CSQtugbM0)U7N0_TAwn^uPGO{hOXjTPYOa}~0Fy!Zq98o(2@0wqS{ zMH1H#>91_S&%Ben{>IPLrb}PdCHzQ%^XpA42XZA zw={BGvMJ!%rpm9e*_%IN0~l|~8y+Z}oKsH0c^x#3mtHeVRW{2}H98F65B$$NC&=sj zjCv;P*Nv8gpI_-YLm4Ua(r{*#9pVRSEOQ@Gj8m97j+#W^SDM)m;%EULYv^NL>|i;oTvN_HT*09Ejog#kuP|$7NmT#&Z0aOV553 zgku>Ozn-^;KR0pXR~Q)O?q{vICMCgJ&JFG4M=eIl0iow4{h-1sj(LvB(ntlJL#`^G z7N70456q5C)}BuLhWsr@*z;zn!PLXVu#y+X=)9X=W72bq7OLD#*1Y0;B>Mib!2sx| zn`N*36)jHjF^V|);1Z!!bP@CSd*FibI(A3jprY>EGqs96M*$xwr(>)FI~q<5_6|i; zF5sVFPw7B<`R35&TOdCTQJr(IKb`p;T@`Ec^Ss=x^1_EY&_A8{6PtZvO^jv0YuIP` zT<}d)mz33;0{vOg`)cJUrli%heuW7RPLyqLSr_;th{M}>yaS^_pnuKUx=2m=x_ruZ zw2$zGF#JKT4j#WUYwRzjhk)NSDW^Z-dr%p7si2nDx85i(bGhuHbjuJsm=*-zK zME-n)4X0XXoNt~lzq@4?&f8?I{HzmhsQIz3sC)$sMqNeZ9qy`(2h9>&Q{~Jg~1bq~m}~tNzW*#|a3ZvP0c!wslA8G6H&g zQ9Z$~E-benTE6_|=dRNzzlCNhDbXueX~jy_WT1Ewy4-dEpX2l@d#>5wR9Yf2{e+um zKRhpM^280~55B$NO)hERAg^cEOB(W)1nxAI*Z=G(C}{g9SU~%idF{o~_+WvdE!-v+cff@{;89g5N9MBDL(WYJUgd z;}J|t8TOU&69t(AGiaV8JmlcmaFf9uio%9L3BT_?=2*$RtEn*iv$G7v&lO6*xANvS zz6%M0cm;YD(vS^THn%ePm`GA@8Io)%UW$63Wmcj22AIF1wf*M1t!=}thWt$ECRDF+ zA0Jn4Zu0e2L3&CP_jOx3m@V@H@#)F3)R0jCv<5BTX=y#5b$@km;cO% zc=F<_Nm$d-qeJGHm*Z817JY~(d+ma@UYFT)&~yy^DPP0B9bp??N-chrvx>VJ^)Cee z6bhxl(b8pH8sfhJ4UIaSdH7|vXsEZe`{n%Ico->!S+s)DNHXry(GW4Ie zO{Kh`oft&(FEHQB4?%pycG8(gB{X#ln{U7E0AF#j9TW#H&usoJ^`&0--(`AQ)@J(>m7{SGwL406qaeJpspW zUz=-foiPabK`2gVEV*&LYA8#4=@sZt0)JKIc*aH%e`u1rKgognzy4~}s(XG`r_g<8 zJLB+p-g07j<;aYLUyWrvfA27VO}_Pf_`bkTR@%{X&z3$^(+eWm#NZ^xv4mQf$oY*)W17I(QU&D{R1nzwWGbdoX-I0?ha1WM7j0Xfw9v$2!!n zfO_RXO37E^{sSAY10OI#2ys5ci?{Ce6MvmSdQ4Gqz_0^({!#~>g46ImCQYN6SCsh; z+Y$;|)}wy3X>pC3mgRPVe@EO&$WO4Z)9_bVg zjfHU1t+4!NPM{{dPvHCY$rLkpy_wv_iYVTQV*l=UuFUmlaMJ&fy@!usuQw%9jP!Lc zcsN^1@=XyF&p(hX(-D((vuls%6W`w(Z6N=fB|Hi?M0$FHKYxPArQDv&8Dw?si2y&u zFW?7;u~aw_m+j*r|JX2+-G5&UyJh&&0nRu0X8=D(B<>0fI5Dsp^w_nxXLm4%v%cxA zbv`_Lf;P&=U0VO;bL~0soHt%b1FP(Z7`|wOMn`vwx%gwisnTCHNfe zwMvny&r@f9bU4)ePv`RLl+pWThjL}M2G4muJMt<-Zb6>qJ^f`J$nTAI^Y#<-qx;-M zBN#P!zGFddqo5h$tCd$PANWCx9riZzAj+R&-&`C$s7aZdbY%}Z@7POb#{f=fise-* zU3}?XN}QKIG60`p!oay2^g@$_4NUcB-JTUbD}*l*|26G9ZyoQ?{il}}8(m7;%K!g< zw!x)_x*CWlBUr4?-;c;%Ei-JJM8xk9VzqSWCzg*Suiw26;!zJqI^(x{{qmYhRRVF> z(Qg4quSJ4i>COf9mwxubzQDTtO*#_1U@-*mryTIPyDTlt%t@wkqBpq=_-|IIA#2;? zK{rRoJ@Ecvzm+zwq=T|PlhJtL2kLha&f$0+t5or(-n)09KPMDAUN|VV4E(%n63}_a zh&t*QF9l>yA4-U`hx&o)<0ul1E#F=q5I&iv9bk3b!O&ayZL;?A;eHlpF6c#Czib7) z23ZTgJ1{N)n9 z3;f9{vhON&7AeiZ_n*SLC=PBcnT{-2ON-*w^lMsi>%ujxfK0hjgr^AB-5pM4&sSB_ z^hEG|CowgOOfouWeu~K(EobG!BYLLIgX0vr+$jq5T9TkAdozE=4M3zlM4AB{GYBlH;{6& z_O_j?k=+dVgmqQ_8Q0)zjlQif+<*KK>{o3M=9?QNSB1Nv^T)DQifUkM&cD;9bNl{- z*9m#i5!7Z;|LV14x8?&r9bgCN(i|t+ueUCy{sMku2rD+MV3u58^*hb zz~_T`NalLp&caGNy(vG~A7SQV|Lu_&!qzv7A$z!A7_~T-MqZuxue_(W0QHMaikn6& z8g1)xoD3{cJj7tW$t><&R#;KRA@n@AhrFq$)9-Ry7Vpr)-5a{Iw&7lF>EybD9|a?? z)Ao2;xOAxT(SDAeoo`i9K8mnv2sX~XU7?;d5WdYafrCgeUxV9EY^EtN>`P#fmO2ffNxm?ykiQc-1R_Dqo$gX%+*Vq$pv#m1@t=C}en4+N`RE}qU! zyJO@cZ0Usi6RnRcS8ihd*hN1B`Kl{IhcWyqr+0pY8|x+Dvq^$6vnYEgpf*Kw>n?X}+luw#hPh5IND zraB2C;Q7LNpu8a(t~r=%-Z*m#>4U>pTz%I;JJkDAi6ag9wHvz{I35^u$(pGpVz-n) zJ>*{*S1&498rGv7gW`pWr9Q4idtH2Ga=2u^v^Lbwqq3weALkn=vl(L#vHhi`a`TE5 z>#~*F(0Ld6kWLsgT(&pHzwtx*15=lIw1&e7qy@q_rtW68i5KpFX1Dw?B zZTQs#r(as4`W>#mx~eQLFeGg%U%aj*=6P|sQz__4T{Zvy-Ej}hcY`dZJ$F>Vc+q(e z^mTW$FoNs(r+KGh8`ri@W~pD6|LegM1f;7g?681!s>VgUYnfnrc*jxSk}=rJ?~e9glFthd+C zRwJ{@qIzySs!uVGsM&GcK)pSexu666GojJr6BBCB)OtFlGm!l;BQ0CI)jBaojDC%Pfi*p_3@4f z?Xe{1T}kngvYrlU*k_FUqaQPFh}ZbT^`IQl1?>wWr}E6RsUy^Bf=L~!m*M2pNEFQ1 zV!q0C2<|JK;Nz#nnPiK$bB7@RV)WyPq9TV^q^I9|Qt}AqQ2-x)sg};=J#cnm@;Y?D z-ZSu>T+%fK!bI3=RF7uf(PVcymxlOn)$@SQ!{a+ylu*Acr*}NjDg}Py+W@vBH$e4T zTw%wx6~GtHV+IDTowi{LqQCsH2;Yy_I1u1UkI6l^S-bl=(o+eo(qCbUP=7g}MRuX(e)&f#AW2=Q_dh^~Hp>yqN~MRQpkIpdQ(h}R)pV&J zF3>rH{5M0j zWIxP|kMF;`{&qCMQO*ON$1UFHzO!1L>N~#VpX{+FE{!%iPcrv^TYmb~j-3?n4`Y;L z3$wHK(NX=m7K=!agR*nPnE3p z*|^DkEa_EI*)ctoU-sFx_-{Dmz00=CA)hIh@VpY`oct_Bkx* zD=w=ZJ$S!aGF1Wa^?-@{H^}CkH#d~r3`_*Qh=282W3ERHj#Q)gfOA&84oriJs`~C= z^t?d-%p#}w(=C(q6=ZZja4NDIz4m`@n^lOdK=}?opsY0YxFzpQP|s$_7yX`W_$bU5 zwV$#)!C13ur!|#V&N;1IP!B z_OfU+YS|&#&4xVWzsYH`RIU0ynwPufLOjDQFb&Jtn(A_dPk-KYts@20p6)vG9-e0t zE60Beod52RH);FhD+leXKcsIG;s-}r5o%$Au4M9GkUugOm`9;MNv=oFhh`-O?>FCA z19N_n$xvE@0(+5d9^(I3wQiqy`v@n(DfQ99}=V&2FqJ#g`AlzT{-U^MbBE7)nI_lt-zG zt*xRWrb$*wb;{RjPW@giy~tl;rX~8X+oMt4P|!X;JA++r>KuapRIne4Qwg;&ZbM08 z?~n5j5MSVEnYV*BSyDmN6*sp(yT{Qs4b=}DttNbRP=1QZJ@M;iEo0QwUIcw!u*WH@ zhBkafOm2W@2Jj=}%yhE9rp!jkzeE}*0Z(=E=3g2WQrVq@gGa&;-&|k)ZJBy0ThlY? zfw<`>LOR+HB=Rct;`a;9p5QOWVBUC}gy~>TKup4`y1JywF&%O#>hHj_XfOBJyPBP_ zuer?~4Q+e|``}Uj$xJVZynkFGq$I!`{2kOo)fUJnMGhF(h2Omgcy$W+hHN$N+N}-g z@dAYBaf31n9DOD}P4nzpu$NE9#@C8h${&l)9A~BL0RCjA6xxzT-C>^Kxu3B9y;F+q zT6HIL_~cf$_1Y!S&-%7>m3sfdnN2SNpTmCMm`3&iRxP7Jsqdt4UIm{s-KY5XuB3CP ze+PbQ&S+=Zsfh_EH=iUWh(D0u$Cj`)9Q8MI|Hk#h?}PqX>OOm~blSk}5$=!vjGBDy zy+VEeHvfPv=IhQ@xpzqrq5k%0xHJ}?&r>d=!O11SaQkhARULqTfFII-QIz^9N%zc1 zAQ9#Nd}^DfzTT&ftUv23qSgiIkVuh$2NxJSIBgav9>C^V-`46I&koNu@6Ub;{iLI{ zs)b5%vPV++Q(!*}l$PN^YnoWn%0C$}F9>{BU*N1Qe%lc(!=MmheZi@Xn(O&ddr8so z8pIo}(b=+Gt<2OKi?l-r!bIZ&_HGdBRZ*w`GU__0{_e!Jqa83_al5$I58)Ahar9n- z;sM52>87&?|51Xpq~f%&$`w20u7iKK9LL%QFsv+-N+*+8UU1N9{vn%FPTrJ2oZh4HrF>Kg;io#!qVxr;;oJjzV9q|_Eu zZw}hf9)w-HqWfBI2;2q6ROFNN0FR9^sS&yX0sfosj-@w&z8mZxD%0&^4`eMkYx5p} zKfrc|OKDC<6|1f#_Cmbj34(g{<71ldeC^zzEaZb5RNEaNZW*`OKX(-F{{Uv>{pXH6 z$@J-=ugWOC)_WX$Kl7a(`%P}ZX}{w31j zzj2!*H~hQL&QQYPVaK?(AOY#;0AF+Nz;--KRL8G*&ulU5Co0r70)NQAn!K*h0oiMa zd*Ln)WqwXIZCZKS0zO~+-=M#%Vv$v=n2++!&<1*~p6zj0X3X{CX8YgWh5g3Lul{li zaDLeVc$aCQ)g9p0Q9Scc?3xbnUz8Trs3pg*kN1}M0UiRrszKRveC&YL;F)^Wl|OPT zQ%lLqNBBRlobkE@`6&+c?Wo~hoL%C9=`y{ENqFABcy5{CJq&%^iZG|vsUdLn|g%-%WVNpxCUdx4iPMoun)I4%r8# z#NTZs``{aF>hsI=y?Z*wR-9e%O4POC{kw0;KIvJ9ac~&^d`AUu3+hj&xO{%~DlGB# z_r{f@==U*dS+67DLEuwI^a}E|rX@yIIFRp;Qwo>ri*q}Uc)Ir%A$}|*Lvv!rZrKKF z&!@XPJmGl{=+g>iH^2rYKD}5ZwyanO@xAj`uVn|3?&$q}n3TL;xPNr;5z4od z;P5*6(hp}=A5uZ~+gQv|lM2szA$eD667lVg>5pw5mafc^(>|*!tZx~}7I!T^btrui z?73zenYwvHD)a-I-ronERd!6`cD*{YdAsFG(5JvoVm~jux^NHvKJXib z4t{>!PalnQANip9kb3fS&x+FJszz0_YQlUw@U*&J+AHc?se9(=zTh0gsiieqjkABo zSAf6dH9E2ns>gj6~5=yj`wc&g6AJgfJ3Jnwqf(ca_JAs1bL zQw4uF&aBrjt$DR7C7?KsBIHjZV~Wiu7%M`cpAFS(DYas1-}kFnEzTWX4d?ZV?JA({z%Tg-2+{^o;j$B#|@wFZH5igR8`d7w3y&R3~)tD{Q zJ4(B+7_p}G1H?-fC7`g_9$UY<>lF0cmBRPiD=HS=veuTj30f_?N0B9b6qa=O9UM4v?crxccri`{mO)q|&) zb3*)6BulsgdNgCUcMj=5z76}{sJnb`7kWelMi)c!71TfWt4(yTJ9hZ2)^U5o zIM{d56zt;JfcR(}p+K5e)%(ZnIR$f+zZer`UcKo`mEfKk_e9T!=^1f?L>xV^TJJH$ zXXwW}%pME~z;B(|+z97w*dKbH%z4hq$$ay-dfkSxjM6VUG3wv=+$$wCynl<5r4 zu)iYw$c%>gt;C>pqDh_B=9Mv-9=TXMzruTn_xU1vp5&UqS$p-(w>3=nU=Ngub`UL!nM zIh{mRqr*Nv=y$WgbFaYP<1P?}d#GXeecbPNNTS!{>cpt8Lhr@TY;1Qz@nL~{c;Hbl zd-dH9HA=Eu7fLn@`>h`O7xCWjG|hNW2z)!GRn1XE+%EvW51J<>w5k)O_h@;0VmdIN z5qefkB$Bd0yWa7;3i|t4$uNl~lDfM0bYnHDzlNsOSo?E+F5jKFeqfiLx0y>9(lZn^ z>h?JH{b-4Np5^W2{S8~VQfSR_8&f)4snTw60s2dp-X)afqy7_eU;_07Wm{9uW_Ahe z!-4+oUQiE|QS_dK{2TF+j(*>JN}CNVnfxIkz9{_m#m??it*_Y1uuUg_^BVgt@X8WD zr+oC=W8jNGKdaV(&`P{v>7&$_Dx4o-1%$K~_I$8>cwIYcEap|7R`fm|wiu#49(oD< z?bJM(uH$U~RI>B*1-y`-y%A1*MY&&f*!8an;k;WgUCi9A7JvIq)C_w65NA)%^R$6n z%cW0_;O}_k6l!+Njsor8ylOe{A8h7c(s-+W=u2|k9y8=GLQCF}A4)YZ*DCb`eg*c; z1qZ0pq>hWHUA}u8@%79YO{;3DXAT~kFQmiT|CT)fdZg&zHTEjm4)A}WU)lsDX|bjc z#C^|zuOG#Y!F(D?bd5N{UO3-?YY|hh-S1Q0H1SXp;^!dlT*9$M>Tu)0hdrX^-<_Xt z?)>y~W@f1|$G4FM{aG-7qo!_0N&o$7l~s3te@4SU6$`*e@voM!zUOrSdt^e5wOJ?NJ>Xyo4=#i&eA@A$>WV=QRUO^=G19Q-c`NNHvOE7Zgx&O@)hjOhMHOuGf@1~V=SiZ(}QBD zcYMFfnh1P@_&iD>o7GB;&9)d?bV2@!?-c5br~RR;?D94Q&94cHv^agM4kR5K@~Hix zbff09p9l0S&F>Q}ZZbjslf1F9pB+9@qa;7O7WHqD_sYXz%D)I zv|MxRR`8#FT*hH`jwZ7wSxMTj58@xMk*!Jn!x?DdX!2Ht0~S} z{E`L!{Uw*2=0n@Q!*1=xjNP&Fo)56|r zYv1!$2d?j!2Y>XE$Lxo40-=WQv$jZKI!+hx{xXr`eFk`!M+h$fpJ(v;;DqgocE7D_ zQ2*Z)c3D~9_0^FBVtKo>F87Bw6w-IW^M(4h9fySYP!ngCxxfCp#3f z?wefI#$Qds5xfFmsABHkg(0w*0XofXAT0J;(FEbP2d1Jiji6tV% zt@4k`<;XuWs>`UQas)AvgP_k_4*jyuqPyh9lCNKAYV)@Cf#>h(-Ad>(|K&Qf(G%6D zp}qw7PrBJ6$RZl-X}|A8f1^p|cPij-{?fH#j=qQ9&4$wf&@Tn`;(~6=-NJf7;0u=e z!y`2gO|fyp`B;6m9onSNE#B*ys#I6k#@uhVNd-It^QSM$Sa{m&I%Uz+xnoM+d+a(LF2VO_*y%I@o(239 z#DfLxEqXjnnm5=Jk6aM0R-W8n=VA5f0GhAC0=xcb``#zFJxPyud0oNtAGMTrRb7Ft>a_XisNf)97N)WOFheNFqE(4yIE@3ersR_wOwT`D_RFuHCL% zhmywQUwQDN8Vi0=z5b0Kf?a!uZA128a@i;`i7NY68T0O{gZ-b*-Op`-uXrRXGPe)- zV9>9iXDhVIBq}joEn!~N%+Wr=4%x#BPm?vm`TArrwM`lSWL?PX@DlpdNX6~(`$k%t zJk~!z_gQdETN|SoA%cbA zeH|n7p~86$qwQ)nZ$3S+(0hsWEMUJGT_o3kN}D%i5QXLg$b_7(B@&B~q`X@Tou4s5 zMuAIm)Qj(0;h=+_Utw9eLv{1s>)Xa6(dSuks<8HTNz<7WuzaRF&3Le106Oj&e4oJ@ z5s0^-*Q|0)E#UJ5qxbmx&D+{MQT}vc*)kE7Lka6?MFf23Ua?C0)V@h%PodE)+wSVF zd>P+shNvG77sKja_3AgH2fs~tPHUlgmK}Dh#Y*`S*Qnk?yjEywr<-v--ze~NpRk@# zGt1J{U#++Ee1%De55(I6PZzgV(_h3_`H5`!k;Ey`N*B({xm#aP>oG;C-cxvgf^9l&#NQNKNG8n*{mY$GomI*A(!}-1w-KKeQu~FF zQ_^=mZi(sb0eYRi9eb}Jz0SZ_1Heb-wNL{6*xl-XoBMdr1Kxr8fq<@-vMqyWEcf1k zeOH(!3wudcgN-tzl>CqA{mEVhPv1TKP4bV9{Pn;m@8194ep1DQ6ULX(zOIe|+6)%5 zPo*2Xm{|4vzxj=_1=}~el0v+17d*c-2{8?=1B&>Mmxk(A-ER5- z`mCVeeD#`5z}4ch@FpegJ#h#xjC+i4LGd*NuT0bIc24rXmfQ&T0sZ|1qJXpVCg)^k zC(0KEsXlgeg*EFFB{RQnK>S^ooKQb<;=83idP1ZT)H3+-*s4EPh6vO?8mG;c~x)GY%b)t zgE%i&7?2Az9-6#qZu)23AetX@wiUA$Q6T+<*Uv3P@b*>_q$SML-wr|lZ#yN$;gTIK ztSLSy4dMad2mKmqepAaG%|HCa0Dn#~i+a25N}`m!FOap-zHmViTdlQuAokH?+#LFu zUmrm%S;^l2=>0$~@O_h)r$P)YNPW+1D=Iz;@vC9DXmpf)w#@~jMcM71)XX2Z0PpWU zIrO$2GrjZs&gT%^!WKEM#Uje#=E~kpUa(Js3;Om$cO`(21pIzUrKs#t@-rp-zm6jP z6Us{_ziTa5cs^eX@`kkrEoo5*c|55e=T+%2VUO14UB=ldg@y#Km)w!rb;-S6$WNet4E^l(DKOYaf`eh!&NS*l@hPCS7G$Vq)3Azxi&y>K|qnPzm*iJMjB>GacqW#{puz`RH~j~oA!^GO!6N4vu2KVFNFFhFDLfO z3h{?QF2RmFc-^^#BMp>$8hW@lg=pKwroyVuxKQ zQ7kUo74?I+W6t^|wwz_QnDR?s#HTMvjZ2BF%-ERO5nT`Qb_^GkVDHB1-ZXw|3-l8* zV1H18y6{?TmY;L>KjpsKdF5I^uS=gTSnck z?|y#iTi1tRAMH2-J-de^YCo!s_XGZjTkqrz1XNdg=AGzt=#K|IAqU%TqRgQLZ!!b% zPdEqa$nl|5%XGKQo&x@T443*^-_7t+#fiK|x{$vul%?7Ge#}YVXyF5V^eCmU%pOEB zZgS0-BK=TraOrum8^zBeJQ?k8XvY;oe_r>tbC_we5~?Q}yBu!yAGn`*;eA^T;-dxW zsz15J;ZGLdlGda13UPNxTBm*O&G1mZg)n{wsA9hV}zc)WsDG{IH*;DBd%iRYx5;O03CnwQTr4pP83cTRE5; zvqh>!A-nP)tQ{LVJZooXC+LHlBRw?Xv zU)|ZVFCO*NVL@uPT&{>_h4W^4gl|luHEplhKVChf)Gt{l8T262@6P7Mh099^pY2{P z%pcsp`LD_zsVcS}@)yn@6>6Ecwmxqt@eZo(Z%L~Bq=Wj==dV0}I!OZm&n3s4_2klT z{}LbSWC?maM_r&F5A~n4j#AZtZ=l`59$KIcBp*L=K7zfnDZ~=aa&;w=PHty!M3rc#52 zA6m@gjq>b{tzBBUeD$ni#~XOR$s6-QMrz$l*vAj~w^010To&z#R~zZk8-3rrI1VUD&n{H&YO%NUl*%U7|Vmnm-<4wzQ^SKQA{ndlB?*#roCudC}(7Z3q2Zzm( zHfG(z-$_LG2bbYl?a=pB?!xmQ*Ac$pN0i0nww=XgZT-6(?>6-Z?fVa<@^^u-;!~9_*F;rg* z_5)7|BE;SXZEIJes}C zSS+`{fg;2!CHfix3tL`J9$Ubpd@;mayggS$@osY1ipx5%e@5VB()4YrJ^xD14X|g3 z4-r5I9&cJb#CGLE{So+diLL`fc6Tf8Y~_Ld%^MqAw6^#?*O->fOay*N9!{JhjibY1_;9zD-J zPQSdc;{qQy^)M0Y9k8D)NX>I*Tr@l0n-6&X;TWz2`X^OEi6)cT2Ki1u&gq>sW1ko= zZkq+luXzi#UIZ`Ok#Z07=T#9n^1AP*$pOqSe78?TBBZ?*I0$ zr{*umvuFDC&K@Oo=lg?x1>>o6xdwb*m=Ct0CnUde?z#UJ_yw59bXKk>lSmaVr~YGbHhiJxjab5Pw# zJVRvlSfU^6KR&vn=2e$T8p^ks(aL76t?888Ln?SQUnz(BxULnw02I`tSAk zbWgD#HNM65C&GXX#wd${{SAOy9^dZL$NyU&4Oi{H1kZzwE26e`KN&evV{q8i^sI0m z7qf1kr?S=x)+GkTg=3qy+eT-uOK1fBn*@IeaTRecD17_>sSxIji10%B)p5b7!CT;UOD` znbGt{j`6Nj zB@y1E)Re`9Wsm+CT@~g5_=`&>ixXWM#Y7e^9VS5iVifa?k8ub!^J9E9P(%4AKdPjz zK`+rNZKwthe-HKrJ;AH1M(g@%I*p?Ev5+!LRZCIaI{o-Q+z-(6JY4o@pWX(er4LG` z)0Hrf$h2~k5s^*ieyf4_1M}lYj`%uK)|MDvG6Vk$_3_mGezRT72R6T~@DTQgIx1#U z=Un6K@5u0`lkw2m?6P;?x=8g*j}`vNUp3aMw9?c%%A)Yl-wghQNu@iK$6P;FeDgxg z9;LGlr)^Zuqy0(a54v`v{EvUei)CAV|Hz`s3RA>S@h^+F)>O&I3UE8|pwIfWSRMMw zA$~{ek}%V4w0@*_aVo3X==QQKm@k6)=^I+5C+xPbzST@^LHL!xcj)cfu|0sDwQx(0un%3D%eDFXMM)Mm%Ua_V;qAI2yb1Qtt)Hu=5FkGPV9Mn(CTRV+x0I}=g!%Y6HC1)`>W-R8?RoV6 zfP>1;w^GmqTTTS`m@*MJ2M-?!u;kc*1H1Y#xNhU;DipEA0yKVJ819X2R52S zhPB`BZZ!XhF)@syh^HB%dK?3qJ*pL1(pAly=$DneA32ha{K=gPeMD~#-2%_MkCJNn z-OKNYt{>C$E zn2t4o{ifgfp_GoO3fPB??l0ey>_XB!dE=(>XIrp;o>5vLn?{?esAfqC?VrynQ!A^o zO*N00GlP8NV)Rr$i&=ULuF4T=}IR5r_w`1upIf4wR6>+UC68z4O&#?Yf0S@VUA-cn7~JeZff zBF!NKzpg$&Q5~N4*s)W!CPT9W;U!#9?yQCD-*V9#q~ZGkzZhIlO8D-Z5@h_+5B1B3 z+&HaXsUfc3aX_=8Iaz0CBOhwZ(B04eiLJ8s1AOKcp~xw75GkGYu7v#pHfQah{|)Ec zl#Nxd^I(M!6qK?bPVyH*tT^XaA^uQ6K4r&glU#2(d1c5Tf5lam7E;sH{1Ozo*NX__ znWyTi-s*pf0@&-XAb-B#Rjt?RnUXJ3#!NxKZxrlFqQ{u#%FefXBfhZSxu7M~as3`Y z!6i#%Ka2=!>6=T$Cn8IF!HNEy|LFy;X&FTORw-Sdm^Vr!YG!Gil6o@a%TvI2P-P{= z6uDQ?en!CWpr?QSpxdnSmS4n>z2eSM$0nNArdN1u<_Y`j$TDIRF59g9T#rPd`WPjG zT2h>S-N57K++h??D0TXTW|s1H6KUmL)|UjBVPsjXnp$L2gvQ|{9jtqdQ6LufnQ}bS zY4|S!*dGh{@)oa12Xxse)%H{gHZFJ<10(8UzZ@zO~D0p>yC zo85-~sTFto$9L>&nAxywi~sn%Ae28!Q$YMR=#M$vOk#UP-5)zI^wc@m=Q0 zA0WSj{9c9ly(yLIH2%A`4(b<8QGxx>Z3%4~8Fa{B#+e0T#l@k981OB;C!Lpr<~j4z;!I2>D=EDHBh-fH}13X z2tNcPjqf!wJzFY#eP{W6aCs1YC*&(IUrqRZW}+awVymDJ=G$POmi9=Ay`v%LUCtHY zR}w$H?2G1^t6tk%@{xo3K`Av7YMP;wQneZ0)z;mh=bR!YD*8Oy!N+G|_5RH(16@2w z*yz;k-#f1^Wyiz$O)-LH zq-3e1&+w9pU%`KY9!@y5DsXLDMN-_?uxodd_Ak6e?u}BydU_tkSBj^J?s>Z+oSU;A z*jsr&4u6=%;k;v15%yloHhzzdV&83E+dO`_?s+{=}sJ>6!MwYBF*35%O)0YMO?I>8akQH!h%jk{{Ts_8li8 zTP(IKv7eQ3_^!rARDTWW&0!>>&l7S?-%#YF8}qA~Jj7$z|LYV0`s~McE<``Vn@?xg z^jI&_-U;T(Mdvp`{Gzqs3LIu_yPr4Fl^4>oH9lYzY}B{6lhyjG1MQ-)9vt-LpniDs z@_bYI7IP(U1BdRyzhPeC(al$rf583h!(I?tT3E}D_^2$5!u}-K=R&nN9QDZT^=zDp zS{2)tQ}(YOj|ZpEZ#kYey!eTOp9RBI<5@jv~{J_p-Y;z0*@?RkorVw%N2Dp$cjhOgZj^mZBQ z4-GYxu%91IZ5DT(Ri3T_{DJiTT}CR+V&xl;0snNSbLUgw2bcpb&Q~9RKgW43OwX1M zrpAe6P9yw+qYnJ&w;iqV&bN|nK9}GG{2E0~OiaZ3ReLbp4@}?$)66V$-I|M@mZqnO z{fK`P?NVM0cjNAY_|cCupxS!3Ia%-my=wul%rhc*cpBlG*(SYC?UiHUtU~$%;EkVG z|EA9ENAW?BZn3-8;6RO|H@*h=oG+9DEwu)!yQ=P)xl@2Q$C;6lm0e%P=>ngN{L4zd zb@qB2;?O>a_`JvF==>=)P!1;U@C(5N*h+hg{iF^9=y%5&FAnXg(}DgLtnf>o%tLua zEnC`G$XDh=OA7FI0|&NQc=ZKu+D5*bbu18OVt@ZrB62Ig`0L3hcYpu9YH5N(7^VgR zuSZ2R{hUi#XImV*D2%_6xzrwVTVdQ2)Q1iTZiT86U|ctJT%A zjg@LT=>Da1?DBG3?sed|)d4;k#9l-ub$M);tnO}#7sk(+Hia42h{CAT3qFW{MOMU|=t6d_-P{a5ZjyO$fTT^aSO|90pH7{Hvi>(}R zI5Iaow`@dCVHxZTfqD+*s8&L0?~@I;bW#5Y%1MugLyoKw5`pNg~H+(%i5A`Ej zA4P&ahzJ+v1GbPOiytP00 zduI!Oi)G79eC9uez-c%7G{emiT>r_r=y9VmrM%ueL=F-vA#njS|r zBppuIa^5lm^PhXyeZC>*wZZGt{Hd!;^Cp`|zO`+J_`+g3yRy~PKi-=Q*Q^42UjYBh z_HK*X)yDJIMgGSmjk=x0Iip>{S_$|C`l*SMpnpv%N%M`(F#jd1QFZE=-4}ikWjo#< zz7K5>BPCSYb{8?YO416*zC&sx6giLN1%`4;5HF5$jf#JD>~Xf9N-`A_NxVq?PY*NE zyvBR7NlYcmMsY;%t;^6s|0@GThu(mJx8InweiuTy31n;4TaxSX=q<#iGn}QL&+s34a8)=WV`^RFQ=F(vvm@lCG&ZS)QSao~ffDRkxg#{+eGrtP^|M-`c z*XzyQFL^*{Ith4Y9CN}GTS-GDcb>AtT7GjSIoUa&{iqBHsn0{g^Y!fPb{!u=f!4N3p} z?i5ur&C6x-0osopMS4?H(@}=E%@zT^g@wh~oY&O%uC4mx^(9>OHRPNh%wPP2&{}Cwy zX<(Cdm-G1w`2PJ^w7vmp^ydDa>+1I){;{xX+3KF&H``B~X3RkU5a{cneK9A>xgMw< z)XuGU4)FD*uhYlxuy#y1%}qa}UwE}?K4$fvrv1kee@=1kv0oebXsE#(#rF`W8B*T$Z@rd_*MTp9d_D;H5SrPcByX2_ zly8KPv|O25>$I9X3@ndL+wXDgfP4%3N1k@es_hv|d|EnT_{TTF{ME6+s}6`?!lkK^ z0!*ee2dOT$VUd>-ELOksSD5hx270zFpn%8{|(v$QyoO7Db8dEn|td5C7MHTs+{f)o0Os=iymlzrWkO z)K6RO0EDMN|Cidqey6=uH9QoxKz&`TS^s{L|MTi9j;g$nZ!452w)I`1{$LrOj_#Kc zA^xDJ|LFeVJ4UdN73n#ZZztH1Tt!Pt#!ImO8#wI?24Ne>O4M=0E=G zzV6{CvLnE!rXf6?$J zZ>ibY$t!3#lyba;`C_2NkZny;EO(IY`EPtx%*mJkR2Ux_cmVF_C*zlUy{KUyvPm%S zylc64&FQYs>L?!9KR5{Z1m;EVuo`M=-nQNT^gl)3=ze0Tm?_zty z4%HWl-EONpVBZFIncJsU@+G{M%;ci`NO6zp>N_Kj4a*q>C`$s}A?0q&mC#a7osn-MO>Yc7P`eM!ML! zpHz~p84DeO@Og@~x{jyG@B4VWtP|N^li((fobMEdQSzva#d_+evIR@Vg5pc*xWYxY+OG!^)kd= zUru&iT?Ps2JGDP3>kR_@W0mfDytyQ;&^CGzW>>NDxKaaHry|mPntBK683tI3by=5Z zJ4bzpeEpt~N}CZ-nBy$8Z@#FXNzjCa>ZHw?12;13euQ`CMfGiY%@^>)e`1cC?r!q> zZNq6JYzqH#acOcHIu9nn`_HFOZ`D+s#dxS5%q*c7)60&pu8SrA98PXZ`bU|9&dlE^*&x19)YO;^C)yO(E-&l+sbiS71KE>lA6-y28zOFQ)~;`GH=y!)@^h z`7^6`p34CK4dx@&3Tg#!j^MX7U(`YUwnSD(k6*-AZqMsB)SuGmuC*#oOaAEM$7BWT zTfmYu3K(creS)nZ37>kw^WdVb9V5&bMg2c)y%)r*MdJ4=x+}I{e8$U179E4Q+vApae}-PrFJtS_n>9YD5&{NM+fL?!Ciynb@>&0-L$M=oSYLP6x}`J^E+1?$~Q zQ;dS}YuB>2wsPk66fVR5ZD%`}w*x-sI-9Z&?603Gr`W7idq64a5)tqN=uI7UHa5|Z z?)9(r2eh35AUl{JMrPbBKGe6|_1R{QxSp<5iI-ge1HTYCTWpn|uPICJumb$0tR+?Ekf~=u z&t3uX7W7+vi8TGsMm@wmqAMX^JK60Xi}W4oEqJJ35V;}g%6h4KBIQZLh2GXwc>h8z z&FX!}lPmkvbnfT{?02#a6zcnjxb~^RzPp#D^f6RlxLVTj=8BMy2r3JYc~g3~p&#vk zhxz~1Z`53BCd@zYOai`Z6a)NI+Vs!1#q4bh9;@;yzx-YjgZ9;VdOD?|c)5UA#$C1l z+*-Y*P7?VO{<+8)xp=w#&yxu;z%P~+Ya9K?FS5p`Z+AlZ(NRw}JSLtQ=&2R8s-Sv% z;-DDZx6o?}-zms{FzS^}f?p zaJ-P9Pmd3pO=@1omXG2La!RU_fE6?!$l#!yew8#Q+jfB?=P7hs6IqKry@h0?})Z#IizaQx@Qw$aRs$M^|!t!Mnh39!r?eW9?;i?a=C3FbmM@fmU?5Yf_ zyC3sKKu@f!v~1i9%^#StLfwR~-z1~EL>f0r`uI!+;p=+m#~~zr(c0BN^Q}NXy|YO3 zPBPLx?}4-)&~K9jWHde|CwWIURio03;MkqJZy_#zHz=u)um53Pry9;LbBR^ zn|Q;iZC$9J3+t1mn)}|3@7zrBZ&41laqV{C2w?xWtOytQ04vaI8)q&}yp!+xb(;Xy zvqBu$#Ew}xMS4)Jg%Cf~kzm5*8GErwU<>@31`A6wdr|FHqDKBW*zDel_CZqA#gr8l ztrfY#e2(y&MyT6PR>eo2D8lQE=o$TPweuBP4|CO{#uAzCRvRzP3a;Ra)V}+7_`!LR z@xL!RMz8Rj^V0@<Q^0Qo*#0KI-mr76$wB`X&XuM=Z<-%p!~_)XpIb)2zWsTl zPji-fs0!=zKNRRdy#@5xsETdxGaK&)$$G5wHUoYF_&Y)43xBy)sJ?2l7xXYD4wV(J zF$#oycnb8;ZQAB{k`w-sx&?S~o>BO1c5li$)hLnGBgo!D0tk4`^5o?ru{=8ZyrHb; zizB@UvKl@D{_O_*c~+68Kg!-#Y8q8)4fbN})LB(i^M#&jm$Y2if1X06HY|!NdY^Ft z|L|~}64^_tzE0=}#Hu3Bwx={=PK#Ph)ZZ+19k-VvyR?UiRh1;gzpdjyMRFC zKD-fRr0S3SJtHl*a$Q32{;#&QRH!d;`3?sDwqkBNo-VWz#IG?fSClsA_)II06b1u+ z;R$LidmUn*h?TN5CK28@)_0h&*ojY6dfenA#KR@@-YSiiME+;si^2agA_=?nYS^2q zqGTHPRQcgflo~GoCZOQnhRDlD{Ss=it26D)61Ld%`FyzaG5Z_9j|Nqt{+0Q<8Vz(l zj3V34iH9S*Jd;*_MeoNrbk+9NYj(KC(5yG~cT6$I=+|0652JUZ3h9>=~RN>>FjX z9zQm#^bX=K)arrpa)ZixFR4iEo_6~aHfqt!KeLmfV3gE9>x%EcM-3@hNtM7h# z;f?qViX&lyLz0ZNd6$xj&R0-K$fI?Q9F5=F@~-8O_d$|vU=P|aGgwuof%sv5gjcI( zb&pZ0U5zTT_mC1BCv|(LofoU;fL|x_Om1k!nt$)JOvu+eiSTX+p`c7kN-ee~N2U?* z_yBxfo9>4s=x3T3g7^%2aGFH_9Sq64chc$Tewnb^SiUqdu|w&za373+z`n#?Ro{;` zcF0(8(en&R4G&i@>17!`tlWX{9p!}_fzTb@`1P)VpNF#RK0A97hdZ^vxS3n;jrP@I zzll)^*st1M7pyQLA5d6n{?slsI#W#jE#iNTV-AnI=)X)2_#Oy+zhOU9)yDIb=V)Zy z=BWjg&r=+_Y)K)}M-tAk7Lolj9o1|?^vm$Q0&fW+e@xa?ot=9sJKcKccVYdx5c>aU zrd1VX&$SWX(-@=b;gj8K`NG{^D*GAGp`Wc%BiL2)9oZgq>twLElwP&iR;sq(L$4Uh zHx|;J42iVjX|uFvkgwuC8~1K{`%ctnxBO5wS%^2}78jS6?v@AoSFA<#6VPAXWk*>X zKjT#13HakPU%idZ_K?n+ej@*>YwI{^JHeeiH6^og`fX_4bfOs#oFi;=(VsI<$`yfq z%SWl&4MES>nCe~fsHP|S>KUiW-U7qZr&gS1V)Jh;@9)h~*#&y}DVph^ zHz)`l)vM`;`wjg4Qe$k1yg*x2$6~{w(AEoGDzI;Pp^;u4e;V#@KaNCzeUfdd7g_@^ zp!%c0AdsuqbARu6t7U)3t0mCO3D2S|rKeP{q0a!nFdyRntvmn8I*I8C)K98CfKw?n zZzcY{tI~Ul34ClNZ>m+bjM{?n`5D&{f8OX&rY;)xy6GA5&Pj+L)KfN}U0?#78U0%= z75Oid62Wo3gi^9qew8Vl&pg%=>Z++`^3u-mlt03spqCJplt5>(8`DvJ^ z36xdqIq+?W*ul=xN%Z?B!J4i35-F9^E|QB-zt`c*JEGfYN!1hTD;@ZL zg2-HR4vAZkA}PZ{uZR6UT~tNG&+4@&wXH$#8Rn6R7FOqEZYA@40q>154O1(q)YP{% z%*j+VzrqhJJLZ{(yK(ig(T6(F*8x2Vn1`IdvYa*C?65yqA z=3(3Onwq06yB}4gKs+DEjzpT1`oyE+`XeV1KBj7_ok#tFniG#e|AYBD9a~FK#S>Yf zmjjZ7`poUi9(&pyFK_$~d@1mo#;KNOG|gdRr~STD)7~`;Sb5&ezEz)IuHC9)-tsJ% zeL|)f?f1SKT&v|EJWsNwc?;*MJbvbM(4H-Mu)l9Sb{6(+ikhMQkx*X>Ci|anb+MH= z)0Nijper{F&#xQ;&A!#UDS0g!_{*^0ik}B+>O}#NAKAey{)fdE+E*ITOJj9ON2%9`5KLj9Y!ndfj&L z53JB&*YkO?P4UChRxgmfHx`v$axHVvITbrHlXzk8e|R!3qw|=_2;%P=9dk=xeNU-U z&778i`c^xJr<-jSD5H_I#sStDjZ2*+)U|;$>}mf~cwuiBxtM<`IhvWuvLz z1T#O!Cn#?HhrEwE+7c&GrTHY|I8Xb4?ntKmwx$) zu#QJAe&+fCZD%@<+~vX!g}%F-cVgSA7Mu12HjRD`U%SUNd~GVR;7Rs|#0P$)jVj2JN}C?-$~@;D^!aH!p^>>tJ8l z{M{otxdiSqD$~n!N832+m)UMhT`X$P4MzLXF!$aTT1EJ-?cQfcJ6NFqFfY2{&b47( zH5ETa`23%lktg0bP@J^C4c-TTiUYoOa_rr^AZ7oBNZ?~YZ?PV4C(}lhnf~Rj=H*p+ zRBE{=+P`u|a-4r0P{vjX_^i;;Ie4rfkGG2%$jLbaGTd*{k538Vl3;jls zY_{#hHKn8rlZy~<=LKGZ@iAJ&{o;2mz&AmCg`~JWuk4MvUzN;=yxYS65q0J9Q19Ox zrA3x|TclluGSiH1SyP5enVA&E5R$q@C@n-GjW&fWNhFnBvlt{sh-uG|tYZyz+bjub zj4_SR@4V;U@9TH}^SWYuKJWKA&w0*so|CbnBLm}0749=6_X$XkBV`BMhX#5S#{9-0 z-?fd0n(1>gvRwSyh-7~>KLmQz+S=wDMW=LXCug-6ncgbci~NQ|>yNydp1!jO8!k9% zth|bS&Y7<88Q}>@u&@Sqi5jm!9#2L3Vslrn$`oC)!~fe)U8H9s`O-7Ao1-rtD|Jgq zf7t{1?+BYLBv2KnFjoAi_4vQ>bZ&ozvHomS4@u}h92UgIk|rY6HX^)4+8LFX-;?&e zu6Eydxqj~|mO9SS}%Mpy>K4piSj1+*b>BtO0qj`@Z2Etp1EldPhoyK zX`DvI?@IE%mIQbVKS0{SX6EhhwkdiPcOCYpfUylt3F3pPHX6T z?biJ(YEc=etfPXZmdSLzU@%rB>4RKSwn7(pPF?+h6AE&Y$6cnzA?T>a=`Psc4fqE38ON%shWH#$NSrQ@r&o!NGKOGzOouTs zea{EjpK{g5)#pW4O3Bw-?cc8#b~j8e{)wGEzva4Uc;e5OquP7%Bws$U1tNgIzZ@J%)iTcGBivw!o4jwJ2AIFEUDT3y^woR+F z;r)$=J$T3FTuNNvXT=twd3{Mpj*pNR@i=hgG6m{0z(4F-D!1XWkT)2B@M>k`7GW## zaYbohKll&dENAEUfnG&?p4>bx@6Ykis5r=Q=_@8P&32=BZT`S1;F-QODX`Ww9Px+d zt~pPo8e;bKp?oH;P=U<7y1SDm27TA(q<)T z9;tQS)7u-*p!{Ety{WG;s8Jm`iu9@;i&b@R&bB!90`v!f2Y0B{_&0+XLWiRdd@|_S z#!qoBC$(%SRj`Knsc{)wHpRKHzf=D{(kqfWF=1wAp1K!*eC0HF{^0MO8`M;k7_J$! z)Sw6SG{eyUvQl0Ud07YiK2iLaG4%~KuPbEs(h{`6uYi}E$E2e3s4ED7_pxoKWKkzBPlF=v_i^x6z>F;6s~kI%Y62k+cR&NPERSH@31Kk;%)G! z+ehGiz`mg*|5~zOUbSs*9q{YGN4#>HpH#Ee^w{U;8|8Y*BNtoFg}D@n7ISU@Uy0&d zXR&cFPdRmmR37hcTQ&0xpGTw(eLaOfPeqB|U}F(c=;a)A4Za5atE6 zScIM1to5b25b$Ilf5r~FyG$*3O*Y;;sqx0GM|N1}F2|q#n zg?Jj`Q&MW^TBPBg%GT-?4@>X=&-@HOLRx15@<=TelufAYBr`40N6pojN4qT%h? zTWYy_S2fztc+yUH;Q79{&hY>H7(2FZtUPC*TF$K$$@`N(m!dW$b-W;&v4CR*kC~7+EX#Uy}s)URV12jOPc@6-NEohI12& z2WF!A5K>YQZDZq_{x0aR$nkI-br7ebnKv=^0PY|7b-}(>y6BJ6#wDc1)jEFG@_9_~ zD;V3XIUPN}3L}RxIyZ~ZzBR2cT=8c7bIDh13dO6nf8G(^LV6!<_@UE2W59=KUTin@dhGn^efwN? z!}AdEONTA6x zXHqQwtvI#t1N^)%TO=1!RwyW4JXPvm1o@+$pQ0bu{8A_Sp@YY-87o-1T{Hb<^}`o} zd#esgkzWVnD!a3j*}fv_^ElLdCKAkPYi*eDZB!Y%>{sAHFON^kJ|cG`7W&VZWkrj(N;p!7+he)9kEnv3Tl}qS8X>{|sY*2lusl za610E{sYB>&=7rQxr$@7zv&tS%LleD$Zv9s!i=X1*hsw`|0L$-J$dgHJ=M;S9|8X< ztl(00x2nS*mhD4lW*J^BI@aO|^{KeZs^`~0JpOo^OU&+hlADcK6=g6Wn$H)g#8ea=cd zhu?4O->Sx+Zv6GH{~bls+hxxk=%?0WHW{6*IOB(Zj%`EpNT7FDg8k}y(>h0&xWRdc z`}W+HE~?iO&RRAKek5$RK|creGgws7K2-0Nu_Vqy+@wFTcdH7VhmJ7UQIV*PHT#}b z>b3UvOz_(=%R~8V*<2pr(TYxtS3~2lmw%ggKfxBozfk(_n277sd7@7*$#Ojf?6WNC zQoiW(X~*Qq9%ybBK>b~9b=fANwO1))Z*E%0tunrp6Z3vzj{?FoTY{hI6NLKw_HhA_`bOFE7G z;h%@bDYYv^+uw^$TKZLrO({*47{ht%WQ26vur?N?mEGc5%Kg_uP6g8OsqM4xkC4~H z-y0f{&%B)1c=x-SL`6OioWfT4-{X+f=g_CXtsvgDQ#!8+LIa|4DJ0jgvr8cYRCN1{UtqEwHoh!U|Lms z#e8)C!`Q-N*nIgwH!%1NieI6*w^S}SQA>f(g8sP*E^wjtvY!W1Az#CM6DhemY&zI_Ln|&dg%2M z?qf1ZGUIoAS`xy4SOkvS-2CU z)cci3I}C~KNY8kJ89fE~-CpA0F86o8k{_m#zI>C9n^* zcjT9CEY-$;W)}^$**U=T*reNBRs2%5iXeyqJottI`(`=#*#pjeaqoKg{@`2CeIvHd z@OwFthxj6XN{pd%T}+BQQ~!*7K5G*Jw`w#dzxp{l1l1cPe51YM{98)e^TKW-em*Re zHNeT(_I&b;Ew6gybiQ}9?i`rkn-c8aa2MejhF(++o!XHnO65^mK<-22bRgC8U_P8?X6au(_xrU6b~?-OVO52mC0V;9G(ZX$8m6U))x z>JT49!4F`6FaB9|dDmL++pKf!?4x~ida$wI`u2mLv?lKG*j(lu`F`t1^uX{JA&n(7 z-Qk2{*g(UCEAV9_g2JqIfq&SylfxCZVhf+GkE9j3m1%w&L=>)+EF~g;EFJ)OoJEv{7;EtT=m`^0hvYy?y*HpNdA z3inR{^I9#AjU6+5E=^psYlZyonLh_lO)h^(_&5jZ!>_@Yl@zAWSUmeiZIBQ0|G;+C z96U<1=J_44#O7tBDKdni~&*45{y_BDyBpP;*KPplxi;E3^Xe#Ii zrb0gL$9w|0tMERHs!sdR;J)=U9#BS_5|oN<&j;>-{`4>@m@FV$4;RExvZtf>&v0JF zcEA=Dv~=b@MEqh!Zd5u&*s(C|LTa)1QrM5jWwBAcdc~1pVtM4>{o42|cCM4`m}^K+ zgxLsa+{^ps9=;TG5cELx{P_a}dfT@Ri?cSm%Jm1DMH>d4S1$Pp3)4r0b>kTSXyXQO zi~5zIhN1Tb{Sl6m;+`cV)bazsKcaoW7{g;yrQU9zKgwqqQ~we8Yn(3ql4x^wei->s zW67>@;KNSOQP7G{6#VB;bNzDOM>+p3AW^~swoO|R(-B!W)ByW40DrKn#(zilX6pY5 z^O)8yO$(79Rz-R7(7yC1;1?l&pucHf)#fs~vJ>Ese$1@V%1SXTIVdIaF4A8x1TmAF zVrSL$1f7YK^VJa-*NK|rOqtUNqYX~7;V_%YZ$~2Zuimof4lxx1i&ixrISN&DRhc^I zLkMu+LI@i-3P7K^TH1^7QgBF)mC*2V-tC$-j)kCAN()ND4s}f#81w|t&{z8ul&HWhv`({ zU*3c|WvnC{t)bgZ#V6SRr>A+T_i)g*w0ge#HkT&D)8JQ8>Ceww2mJ^_ANI{!>HTTh z7on4robOGlvECYN1jun73*2wDftKF{P50J!+hStY@55M0Z+mEys}&+lRfVE_ny{c zkNfVFGl?i3jfJcvkGXr>+<38gBa@2iQ7`5hu3UdhLHk50yzp~PE989In%0$gr-`7Rg?&e@Gafn0=FF^XygNFJ>200L zT_mgsis17FXtJ+;N~$D4V_zaG2Rai^Y%AE5LAA4;ZA5|Lh;6rxC|w9pHL zX|{!^-^>Sl%G$=+8)5>7de8oG^YPlh1X1HM(US`wzj?^{x=6F4b7Ay94u2XVqyBOz zdA4zH(Ik0ln^`uh@1@=sT32T|vOg=T-G%dn{DfUMCu*4-9~$>cFaLxMV>2^Z?52>N zPHuR}--1dP7yJGLE7h8^Z6_c1MZNi2SSIgp*w6;)i$8a|9O`tRsvo=<@afGw;By|l z!ICJAVBkP;I38L7@$6e@%jAmXGj=;2jt%mW$Cn4hjctyO_@7Jk=c0aPXjvGYdwFU^ zy(iHY_^V+F{WZr(sKoy;2lTJ-|0T}Eth}CGg$f#liq|HhsIcG45B6<_nXtzKs_C9| zXW9Z0>R0A&%aFi43HZ~z0zO_=Ir9_=@F(bFZ|NCSzVBSz+qvZ}vp>qXXT8}vAWa!@ zO%F##ZqZ?WZS}KL?uqL?Rnn@^`3ZJ;xc}F>2g6_7cxvc=nJ4QtRXx$P`KX4!59f8a z0IS%-&T|y9nAxA>uk-%vXDv5x&1=(0Fa3xWh&f*UEGApil0AU(NrmgPR|}M@9}~I3 zy&E#L{!Z&~uf)by{ssrrCF?QHH%3Z^W1ELYL0)6TUx*K4c-2^CWZt*)C!l$SN8sPk z-SGV4jTub>Gh29mX#SGgEF699+v|k?v(Pl82SmN)mz+Pe+m{nC)p=5 z0_O+tVH4HXv37b=oNRKR;Ff%HWmi zq%QKGAQ{(Oz;oCcTe?6`y9?q&5|J*u?wVHS{ipS{>ZsaWLK*noiO=tBlx>mMw<*@A z+_=*t0|$XG*^T-aL~?V_GnJtwU4VyRzOQY!rGWg&gx944_1>O|FfS!V$El8anx9)@ zo_78_FTts)DX3S{eE@h#WbWJen;jmtKhyoWKEiK|T-d+lu06X*EA2Ye%Qcu8JYUngsxob<%y_d7wmExYP9PQZlLw2_6`~4{cgik9d z${a)E>k(z^1Tlz@#)2~0>|gd8tv3iA7R$7vRz&7P&Qc1W&R9-B_rKzSQ#0+td|hjW z`Osg3d5$Y=YvFIpKeWAqQsWcopTEFGifOL>G&urXyCgc}qk-6&? ze6yW!#;Lp$@cv;Q>>z!}KqvV2bajZyz6;w1dyfr-*1&Yr2v`vlE8yQ<-aqLZ+B*IeUKU6S*G`ZRR}4 zzh#3owEq?S)q)0U@p$2k!`wZ`8zSNTQ7&9GHo8|?GX7Ho#Xp8wMmm#4+p3K5W6*wc z8Ck}`RZ+sdOFfF@ez;~AF7~vv=;;ou_dxqqFtW1YIegsX7?f9Od*(L`Fb2OiOy&#mayXQ%oH^p8s6L`iRs#Pph`$-|CW#AY`d8g_V;SQbD&4MLgeHTKO~Up zzy2N}Ait4m;puqy(pC|+5b;|Q7u}OW+i5Ejz8d?3A8tM0^~&?o;(evo#-Abnf`7{! z3h?XGwr>Fcu6R7}zy7J7i+}PCZ%bXdKRhGkP5l{H<&YW_FZgDD+p;dD*-q-rJdF0! zn};Z~i8v(%k#X4#@GrqagJtG8+=pR*9sL-siTrNvv6SK=Ogwp!Q{xNuHR$_hWKu$} zEFQ63VHbbO_CNoE#02eG^-wSE8HasIYXwwA%hb}cn7xqC=}uqe{xgG5ctrWW)g8H6 zarf0T39t_Y=@li`zMtlpbej~;D?d2FyhFd0-@X~tER;_6G;I#B7pRsx@ z+H6CUL&*b6-vRXb%)Q0~_wl9|3!1NmLB9(0GC$kPRhPWs83nIOFX+V>!#7=r;`mcE ztHl`b{{#Fe#pyE@pBqe2@7+N7qk^})Ia%rXqLC$79IEFle4o|uh>m~6RFRW_54Liq zF91EOtWxP$V<+rK?Xx=#=1F_FK?mW>C}#(eG?%j?@sVQ%*1IL(;$ zTv<95_5a+A3Aikov$fSkdwZ1Cy}U>o>JL(7V{&~w$(I{`F?Orw)EXZOoG;jS6a)CX zU|DKmWT-qJ1l3SWRG04z-TVpW>oBqlb~7c&DAV!Ft;B5f{fwM<_B7fn@Z z!%xx${6{DDicP8Is6L+_`|}v;KUBcH71XnHulYfJ3H3o)jynFRTidRA1`gZd{lk6b zYSTSjq+PlKKe^tF{(9v`j;6sgrL~rJWQYg1h*Yv*X0CVFYlu(aFBTc|!pAMmqT{wM z=#7DI*-4?&_|L0v4N!{^Kfk3$x3=khRAjuC0P-Q+r;=7f%~(s#@1F!4AYXRyc|;q5 z;FiYW)Ryu>tZO{jGl(9Wr=la`9z6l?zlIc{M;7>8NjkOEosaSt`2C-qxOyov=8^wg zd4I3Zc1)n)9nI6W9Yy)w7vl?wO^$(IJ_~bDeJzd3dHd5jEw=HYNvK_&tXFnN@)4j` zyeO#SRP`D^;GZ-D5#9(YRZ5rhF{L%N_)AIII^mnAyiLO_lAFFo!+!hCHf~2OAs!8r z#KBtOR|$Sc7MTq>eYPq+g&%~*WLtw?nJ;NxdYUaJe7rS zt?qwEc$;)b?YA>NEv=((Sq0J;GhFKWhuRqK{#%Q+8{#28dOgp=8+onQ{IoC${F?r| zzkj6IXBGGF)p6hEY!CD6pjQ|y&(~oBp35ehZbx`%a4a|EtWjX$fyS$7ezxkbMyluB zzfBSji6g;pvQY5_Uv_A(P8j&jLcIj@o&!xi#6sl9bfktxo3EGhSJwxFS-MxrD~@Xx z!_ZOHRK6n)?VGR2eXg&+p(U3|S=V=4)o-)VaiQZ7)LUb{>VVH+9(4`TckE&0{gHJr zp9Ap;s}?jj?d!^P`dJ9^(_B#L3SjC}@YOpVX1AF;=(gbhSt1%1mHo&l^ag$f^shB! zvd^D0r>kU&CUpF^9d;0BPf5>I_HI_)^#6X5dV=yQmsOToWVxPOJI6oom8;0L`srEA zWhygUguo&F#9~+3Ohnn{6r2To7Y&%mzhyrL zr!6%N{%5+JZ}DQUqXpP!`?>Z`Lj4DN{7lET=pEdKCJQd=SDPPES5eJ(v^?Gw#y!=Y zM+&`}^8+`#I9<5#%x4w@_{n;VExcUpylA-5_?gB?0@Bmfjta%0;@v}q zIC=lQjh&IP@?ODsWUeK={|=07ORmLlS(toaPAqfc8ZSG}Th6!Lun*jV@TtT_`PqxC zZLwf{1<%(2{9GJp0nIMJ4$B~3ycrL@wl#xg8JGHsdJ*su?4QVP%}+{dF#hHK0Pr;I zzt-DoFMJ<=uk-s(Cy1v75F+!5ub-8I|Yo5 zSpKoSbI17Y<$hHI4uQv}zKN@1YTKWHAC17{mra035@s=oIWIu_G)O)KGIh_W`EJU~ zwNO9RGTQ&7bTVWLJHw)Wb za>7(=J?bAsfS(J-FWeYX;s*RR=(UFi2RF?iwbyVSoAyQVnDYI<;&c6{0yfF#dtc?w zQJJ-$wDLwV8TL1dVSjwH;NzdY<7=chAz$<}uHb4Mpg$pWHBW%&*9rUBPFY2NUCT=) zTwc7o{DN+IPU{VdcIP zkcBO+K~*!=pg+;azgDuq#O|s3&>&mh55dZ~xV4yDjL2@;+lEoVC*@p)^QDT4bF~#e zfS)z=|Db+@{0b^IBB~F{a-8g6aImD{=gb3gJXqlBu;68}M9yrzFSGsc~i{j*drkJ3FeK}t%tyo#?XL%6cf#&Jx%FfzKogxD2H@Z76 z06s!i>CeZ_Liq<{W-!^C|6X%yZse2rmS^Mt>fL&F9RKlaHv0W#*+knmM8#QeUYqW3&MgwHE?IGlQx#hFuSdG95hkMWAv;}ra* zyYqywU)j=gS0tKe$zV)6GiL5vwbwA%!YRSzvSJV}zuCCY1Yx$-R&*@IIH-{s9G1Ahknw@~h&)Lv)pEfBj#-A`jS4hWJnG2%Njy4JF&A)1IcPac`JJZz}93vxwgp=8W<;=9T};*~nW% z%*5YlfcMR)9{tOtVC|z5#%;I4=VjMDT=W2g{}{?SjY3o}nfccFp4zCq?j){pH^NI{ zPFqJGQSzD?LSS>3j-#qu*{7UDxqHm-lYzW&gO1~s_%kP6F|)gC8Xt_{XM$*^whFnPup=o(B7@P`@kGWlIt| ze^~A+Lmqo?T4U2JKcu%IMR6}{?t2+zW6%QqHSo`VwcKzQ`PO{RGHW)v-(ks4nh|T~ zXCE7HIRWz{wtX0h7r1>%7xY3EGc_VffeFhV@-+OD61##SFftiCLK!6qH)y?Hu7g z4TC;l=5noEwVuc&g->r6iP1cHC^o5aPB$RYO_lmgQ^eb^zN%n9n)O#2C!EtggH0l`Pz$}nTfB} ze@tPAqrYG38xzg7oOs&*_K$SVPBs$z7Xtnx?c9ye$)djAZO~gD zUY%OBNxvU^0_Q&~-F73K*RQ}AJT&B`UrE!kjYN15^g%RQW7joqQ_x8iFEG7)PFB{H z!#Aa2knf?tQR?4FMRUAjpk3Dck$Mt^ITE-j-#YNQD;`;-U44dKyuTcm7YF)r?YQaC*Xw+EYN{kx33|8 zC?WaJ@K5{JJ#YO-3{Mdwwb2)hfL{O>=eB9?{O6G=>#sun0eax0o~xsyr_~mA2YDer zTIMZylBYC4H%R+1JAHc>hKa>y1J#AqnvFSf{?RNVA1~CjTpm6x;w0iz(NHOGAYI_>~&F4<*rHwsWfpwhs3y+I#w!;1Ax2oN|2k?`~EK2!c$NP)wmHo!naQ>?KE@YpNA8x0| z=t#%mJoRB-;MdW%tcdWp?&#wMp7g!V3$B3vwmkMmVI}IvFnk0YymNA*j>^dj`F`o7 zALKEio{Myj!eM#*Nd7@vpFo-Q9jmC6=j%{gn*Yz~L2cmQiSPr-_#I8^dd)D&PYiz_ z_y_yuaImSg{X4!`64#^nQxcm#n7?l!ks9P3@Ap5wNkh^Nw=I)r4gIz`Ier7bNAOGA z+F=3zzMT}5>kx>&xbybUf;xn+D@r=ciw(E0^6RvbF3)heaJT$c6<<8f28!MlmOlB& z-w(@h{_V^NsSj~obT-#xFTIjKR*ALCg2K>z2uVQ4bIB(dhMDH%yKRn)sJN<`W^4jMW+2=Kk-3)7zX~< zjnv4m{>LS~a9_axcScjSa`Y;tE%7&PX5KUVL|ilt@qd!uGs}=(!OZu&RV|5<*^`+as5oyVj}=>GEC>jG?J_QteW8Z;um@(QmXS$KN1!S{=K+427W+s`;( z=lj930H!V;i=x5r;pRst6;*&f8|L@UWI70)W~?U}|B{@5f2NdbhaT-6m*X|E^3jg=m+JRcJNwR97MyucD?wH-y>xJ+aVXR;wO^$! z%1SB&=E@sgwtJ2|qJ!QEGgeY~dB4i9gNORre7bKPeva`DjbB!G3ZdS`yzMOy;W5&N zZL>yDK9*f$U7rrN0B0hvx5OX&-@NONq+?q8Kjrx_)w-4*IGCDra%L(#kMYVNC&AiU zOnZaFOEj!LQFeB&YiEll{@6k34O7 zK3_vil$6wG-Ep+m`#cTxdn3-=%0ZPQd%hx%u%`){Zu1U25gXOfzs}5Aecc|9I6GzM54xz{XRK*eAPWCnP54x_6XyFt zU!Cb7`TcUw@1%Yz%15DZUMni#UarlUi~9)s#D+;&Q6uFn+?vRgrjLYXy`4RaQe2s)$zhhGPyxDH3R~V=%jYj6G-qC7Z(ml#4caZ|^LM`cOMi9m0ixJAZkB5F zo4ou!=$DJD6#wdGz~}p7?yIPfu0Aq7pq~o&5BZY_JNve}wE3hJD*;}$A0OU!r{AF@ z@9^6FZgRhxNLFU-{JC!Nxz)6Idfk8h=iDOYxw)SqKZ?vL?(f%e4%GVg{zHKH(vNw@ z@``M}X!=;6y)Z)DB(ERRr8l$5>=PPKdy0kfeIhlJ1q{0OgKDkd$BDe)yE4h-Ca+=p zXMK+^ZcB{}Dz1Z=Hn2yCQ}S4p+1(L|^H<%v>{r}j$q`80RX7uFTU=)m<`><{1eA8LXleYH=x8?Em z_^9hXn>B%Nj@UoghUUAs@bk6M{WSN{5215a9uU$_&W3t(JTyd5Y*_hKKlQ_+R=}SS z&$s1rRoO!Pf-8-EhMwEp?7zYM3|3h?*1|;mNTqSC!Zd?HzF%{T!U#7X^#iqi+~!lO zP9NNc^s)>WE<1MqT|>`huc?UtmE9Jur33&@%)y+{rXa2rVjxyKQffL<~GU?m>@=@ zYD#rCx%D4gG>;PocGm`_^HX)Ueopq@dnqsdr?XTB`!;<4xXW8x=)H3Z?rb9B`^pUq zh~ZD8Pj2c!4jG{76of65KhI8Eqh7=Ibh!=k4uSfF}F1pRl zwQKf_yUs~L@tbkA2IhTeg)xUKrW!T^e|zL2+t~TWo)n(wCaT|p$t$mo^u}Ke^zGk` z>Yvb%I(qHa8^aZAsK@PgLA@Bk#t{naHdr4u>orwP&1HiA9Qg@k_{bc?!$Za%u5p)t zz8|NWH=k8yy=W(vq58FgQvPEz_+Rq3+@Z5_le7tqZ4*aO{~~Nh1a6(jf|u6&Pw!X{ zel0Ry{sT;E-$AhWd!}i1lxcE-7UWx5SZGI`BkYHT`PZLgBJ59=U$GnZuYZqI0KH0+ zUxI(J3dLV@7p~_1roM{No13gGzX%o~{s8xTRl&(~nXZ68fv?9k?-EX{h#BOKj-0j? zV`+nSo0h*L-t9<%di-JV=78Bu007AJJ@vH;pOD%oA-=K8G^8-5pulg8+i1^ z9FI9Aj`c%wzTP<>H_B-(Cez}7$-kx$qHtNarXT2>ELv-N^!45Nl1nnl`6^$bh5FN^ zLN7Qi48P59Ro`w*JAm{cd_9QA`|R&YQt$}~f0;+@&YFXNU%}n?a<&}}_4i&iJt^L* zQQmLkzaGsuQ0V$pAt&`-js9)aFC!gUUU9D3Q70yQ8164sB=zMgWYpd-v~JmS5Auab zav4S(5*wJ;@3&GR|4JCFoN1imw-yKK4YN_do^-V;A$!M)ksU3YI%Agv$EMhvuf)VM z$<=3t2j%{#XK;BAO}hop)uvjpBx{^M}1iE$B#3^Ga_5;Nv#O~V^eJcHvJ$kl-JATl^o+1Hm8!ZEFJ01umw({vBnFJWAx5EO5f1~ z_0K$>kVadVRCQDt@u6WmFDjK7y)pPTH#!jXT^3G_4-P@RD!+8F!vghl`Sh(E{G;p* zi$9rkHpIc_+nHHJlV=K{59EAEqj52ONIjhfr zH>vXs@~)c ztY_6u!hLT1A~r<(QKc+FNZX)2|6Naci9G+gveL7nvq_!8*DIlZh4~M^g9i^DQaY?7 zL3li-4=dObwN_tt+2H>8EPBl{14CQgjgh31cbNo}x|D ze4FxSqfK8_Im}lbMfr?H6NNQsLj3a~WOR;C=Z+-2&4GD`VZJ6#=z(RZ^?Zzxg}=P% zZ?Wi`e4b;qz~3C2*mllqQT!wF_`iIwV+7c;jDYY1=z5mv%5aF$| z?L5uopU)lrj(#?=Y0uo*zus&g(xX-jYJElmti`0Mu~Dh4(&@yvUurhifx<&HC*YeT z6>l4hxlqrJSG-ccc=>Ikmfx-Ml_)+`L~(JPOCH*u#f(zG_u#i$nU@)!`%{_z6wU+a zfv)@zkjXO*9b{b?!arrWK%Zy$v|r@B!sYVkV^YIRTK?5MjU5eqCEB-IC(@6OZCLOl zOU~CyUa_mUasn0wZ)IJ+truv&zsVWqfhFbn;h(r?t0A5eqvZYPX7}%=2>*u$&#rQG zY#~2%Sa%Tpy^@G`gH0;7t32mLcLV>^i|MU;6x5xnAA6ExA0{H>V zN>i~5;hRvVQF;^NMCk?c{m{X=MZQ^&`qtcQHqBUkIQc?f+m)#vOhxUz#kA!p z|MGLUl4(M#xrD`Lf2EXmhp^Kh_5b)gKu_OHetqXq*QnO4o5lTq3EajpQ7G%^upZo(c19q($U*kE zTI=2wvy9#2}tYmBmjdQDVz#i@DBFd<>O;`HwbFID(7syJc~cb-V=^Fd!r zgN@dn?!B`4BQ1M6vo31zze3=PNIo!M96hRW{kidMG|v%QZd9C~SJ)q0>L-PMV%6Ub-=YzoJk|Wp1mTB@ z2bt_C^7}&lJHc#gk_?&Cv!bk%Q(+6an&BxC=8 z=21b7|FY07;tI1n_e|MX+L*%f-XLKoco{ONfNJnxCH+#d~j z#);sS6(((04c>frB8lq2UjBUS|cJju|blt)2X5k8n1i`I=`w_Fkm(#~P`2==-gS z4nKdsRbzJTYeMyJrB_5IPS(9uZ~4ed#9zwJY_+%gNY3?MMel`pBr40TArCqvBjp{<+F$?IAj1UVJLzHpQ>EtvEBRP$ zOnV=@{Bt3i=YjbRDnkwOdvDm+e<0zv>`3?(r-t}uv!gL=$I?Vi%M3;rs%J1?webDD zC#c7Dnis}DB>#6_?2rng)&B<5i6-Rva_G;ZJ&mWP> z;Wbp6wr&#}^g2)<_lNZwt2Q2XKGWud_$G-<#!pB3rTadppR5n_awh6|>)MVkYdx@R zvf(GhA8$%Vt>Jr3a%99vwT`=4r^Af5qr+j9AA^_AINNvU4bvd_7>Wngr=);ap`TAN zDyn{8y{1@&-~VsEsDpVLo0bY*WuT@`srxFip9Lm9rLo-Wz%-~oCm3r&!g`fCujicm z8}M|!xWZK?<@^)zc?VNcjIa~`+0s#JLUo2@5NH+9KkDETIVNYor#HrZP|q2MC4ziPZg2S zc?fnP{3G#LK)ityA4l`tj7=9PrN!E0vhHgQ`2C=N%mn?{wruV(Kj1GxzZiK-k3QBh z^NhLBVLkF6Kf{(1r^s=mS}RJfZ4Z3n(l`_FTkw;8Pl|n!o(}xWvebdU%*$zK6oTFh zW6EkW^TtV2rdIB{n!X**P4X#{2lx8 zH2nOH;79HEY$fX-zC^EHTkc19M=kRdO-u^yxORQke8~59t*23c>8D4QHuwdzHjnQ>JtqLa>#%bz&4ovyzG!DeWO7;@ zQ>V4MtdO91T_Ff;Tt`^=W_Q_iTKc1p7#|Z~*i9DD6mO*z!~FJ}++&a(xTR}#=vL1DS}jN)x7g6wxDfT@uSMxoYg_Unqmq~qf5lQWx=UkCPnZ7}epf2zW{%f zb2IyC&QHCUB%cVsaFbr-(hbi9V??m1fpUBjF<#VqMJ48ax_L6x=e>-irdle;VQVa( zGkexL&gNU|TmVnY!VKOY(?s#*{xW6gVHdJ407i6Bx z2^ooD10OSOrQrRs%JYu`SEG2$pi>&TZMw|&tZ$95q!}E+Q%)!)aJAv+S6$V_z+`08*!jV#|aI|ay?M6=hm!}Kx+fmTpcT~ z4_^=dbL1j6JQlmCeS8G?M8LIC*QAmG7+Cii^ zH1{sEjO4|@{R%=ISI#=I%3C{wbjuV&xj(yoO^(VN1rNL14acfhseL=zDv>eFh|Qc? z6A3OU1uiYfe^KQg%#-2~ev6=p+q&~bJ~5JbqyOzonV!~uJGBJry?#cJdU4CIlr$mC zS3|vZ{F*pgk6cS_D=984MEL>Jd@ggxR!1g*tOwPXSSiusJWeZa=*IFoh;KCvvr5zT zO5Mf@?Q?7qKW84wwd-zqt|mFu&3$^OQ4`KTLviDwl$75B4E}vTDe6k*fMVa(@lKdg*A zP5g?khsRzm41c}vAz3-~8@v+!&wU;yg@6aQ+#c|@KlQ+z>o99O%HLQAovIr*pHx_X z_9F0a8!YsD;oC9B)_%ceG(TB+m7U&FwRh&jKUYjb{aw#b$}Dn}WyFC0YbVk-V#zqz z@6=<_^zj<-jkV%RPbCF)_1L}FBF;u3{2J;@$gq*USe$qe?YjWJ*sHa9ZsOhP>0Ouu z;4Rp<#)WqLu94$;`aURrW1Y6Lz-gIrl6aD2VtXL(FzstC+Hc5JeGvzLZwDh-HV~yc z&X-x*97p{`S(McyW8=>Cd~PLrpQ4H#N^EwGi-sh%+ZOt@BI)gwqn)_w@;i4=Z$$C4 z;tGy#r2bCt*lhfr_#*57@KJkFQK|WEs2_SOHxUM=qz_Ie2bsbBqKPZWK7#`Nl-I`z zXkSYGL|71!UBz)T=;*u(_yy)iBZ#WC0m&)uz7PEne@7}0932&jhj?G6Ys=%$<#+BH zY*pgrgF?Ws;BOGPn-jZyu2S_vpPk_g8@Ak3 z$IsBI@ipXY*^@E)CRN~<>Pb$pKbp3UXw_BZ*H8!VBO>2%DSPpizvlh2^ezsm+fV3I zMgC#|!u?6|dN1-7%yT?f2{IHr%vjMM;WF#I47M_d?JfOCYG+1Sw-su+_W*y_)Mzl@ za!tKJI);HJWMj=qfH<=Z>6@J$j}j^0e~=RRh^^i9TtC@i#;on`o1E-TKyQLQ8Mb>|4)^h^WD}Q_uAsi_ z<2>R==+9JRS3qw{UeZKAj{G92U(LQ;jKHmRq^Vch&70`EW8Jg9!x83_&exD@fp4UV zq`7(-WNr^ne@@~^gE9Pm*E+W9{tx?L3f0$PXQtMMzfGR;JzV`V;5h;KKeEW=1%>uH68Qal`pR;YR8*GR?XcO|@?Sl% zdt-@(!UoIEb7FL!%$dh)`=j<(#3alT?HzP1&NN8c2zoM-=VQ@sIUc0f(A~p2)|Th- zxxi<2U~hQ8K5h+b6RwOx(%OmiqyS&!2H~c0%P_>gX9U8! z_NN}rYZcMwH;)VyHB9bKP|%T-LO$xnUX3?L$<0FV=&KBg5tB{MWWG{+=RVzTH@{g*Pr@84(Q=+Z*Pw!r=Dov2mDDWj%8x_ zcwSoBk%A82AF?z2l|qJ*yq^|ziq?DjLH%W`^$CcdF$4Y}>B-&s%r#wmls-iyeX>5z z7?v_+zjJJ#u1E7DS9agZ`^EXo|1Gf*__umSvQlQ=KEIgGMz_a+KYOvj8IE+Ud8w!& z_ASC!6<)X^P0hE&_#687kv@R&m`rW{-L>lQ3IBxlk1mcDq57wcY}NI?Jl>#U$Qa%q__0tt4;|VTnH}@~7>4#Ul;y6% z<9lPPKF{0a1AM1ga_gO-5!Yi(zSs%%6U?WkxEMJ*yTxzE_FZepw{& zfABs96~?XFC5#qR8-zDP1M@Spla{k?*#3n1LhxTCPJ+LN_+0wh<}l_$PkXoKg|`rY z_dK}UxZOF>-rZq-3G$oIzwxest1%r9wq4S#Uj)dGEdvj89kuUVF&vIDgu9Sq!#=6554|441W3$AN-at-_%aJ z`Q8{lKlm*ofGf7X%D_*9(Te3jQ{r+0J;;)R$_bpgM_VT?Y$N|BXhsb;rk9Q13w zq$^4Utm?qbssp-Kb{pZmxpIqeSppmX2d>SgBd(PF>UwWSM;TS^XTG^YJp=yVPP8!x z$A(r+Y%=Tj$rme*zkVT+Fi7VzkLxIX3HC`XjjZgIYJBLzWYZphxcFif7xD${W8$`m z;68xglFOA#tBrbQ=xBZFmDg7#K>>oyp1lcGTm|H}#&>09gexr9_fZ&;`^QSJ?525S zY{!3}N8-YM@}dt8Ca8Z?CENT{UV7NNyZU7>!ke%UnnkAd7N7d1HXEL= z*v#qqtL$|e>kqvDDfe5dtZ_G4S-X$&G1`LHUL94GEB8}@eyU)M{(nS$c|4T+`#vgV z?{rFa+Egfm(UFv-AxnlswjrcFp(sS5F{oruvV_VmGh>Ft*kV+tG9>HB64Q<(q_Iuo z`Q7jNp3m!dUgsaLn0cP}axd3?-Pi3Qo3pofISuATVZyAl?K;5c;9)8>b-KoyzmuS! z74_F*>G5GkEvID^*IhaR_k+ha!x00xRD=Gy0UrL-+nmppR7%$9_m*|K>$PD2IVZZ3 zyAVHli^DL6?hnDzh~DKi`trK$Y&bf<`k=qO(q&N5WVfsT#?8wjJ-fr%L3e)vO@drD`=;B421zuUdq6zW`Z14@1Iy7o;J$iY4af(lg=ahdye8(T(B1(fsvw?cQ~4z< zou=Wa%CG*0sU_POpzpIZT{k`T_irhLzgK%PXiCQxl_~Z2eQ8AfCzl%S(R|B;Z{?*$ z_*y}Nj>9GTtDhi-q(wfWKG3bm+9rl+b+r z%QM^HGnR#oKB7^nVqzp(Xrqdim0LUNFA>(_mVJ2$&&v<-bko(*S)lmXB`GCd#E<-6 zJZz}#xxRTC@Q9{5wSl%qns@AUhrSs2kMSUj8^dm-MTvi({iy=}tQ9NFrP)ljRmCNE zd3iXHOr2~MFQL!>c5$!#n%Nj`-hcBuU%TbukRFKstD2fJZIfK{KGo@oSTxT_z3qsi z&nvA>y5K*Jr(wT|jO0Z2b)$R}zz2E=FrVrv_i|e0MXBID!?|U}cq-sgGohaTN1mYg z6-zCf=#U`RPLkWiZ2Ta;&S>dkYRk6NYF0`$ax#c%#djTGK5|gt3(u1tJrU0frH3p5 zPr`iHZxbZ?oBqe2xSo06SMP#N3i!7a9|p_^ulw$5j`HtWgVfxdoV=k;gV~7B2mPnJ zaCxm$R=LVD?=y?`gg4yvuhd8VPm)G~GTBeQfW9bQRSwtnPSWcF4b3NHJ9FIBse_UH zqWit5-U0JE_!24}y8B;&JGzej7bO5qq%hyzX8IF(r8dUdg) z-}!8=ZOFYz?CO)iNnv+@oskR-~ zZ-ipjO(yRPWBDrk9}?+toH5H2rytG!<1gwY!uQyfk~9sW!-fu}l4Run^@24e=AE`m z`R$ys?$J48N^*E{2jMwe&$ag;eO*FInfX(71+$a;PEiu={ujTzk;%~a^b3oPwW$FO z!~w|X@|uD}8y?v1se^1o-2G%V5RFeJmXrR|=lEyY`F3-jC_W?_Em?k5GPj8E)=qWZ z(!f5+K%oDoPVJ*Wf0aEvPo&wkYdDF@$$L#wHdAnZEAhEZK_}~W{k?_ctPNNA}2OJqv6ow zgQ2j$GeTW!`$=2z9V*HXzwbcxvI3@(f8_)Hq}aIcMJvx(k?c%F_*I_YK3xwL?@llK zhq?TjnJ2d_sBfX)mk0Ab$=qVqZ7GL0Oe6n8F^zC=!ffNa4{O1Ffd0=^Jl)dXJfAT( zpbzH{`oSyqI?&qImECv5nSW2x>|fq1GmVApzx>PP_ZH`(^bOP;?AGXGa(^X-0Qyd* z&Rr{$AzrCg^}Sz=;$KQiMA0xcD*ybOKrisu7{SShPusH1`SMvq)3673wD!+^gyV)$ zjJvuZ9)tO5E(NE~*~;CzyL=JEW6)3g_DwF8Bge8j-Maw%qO!NILUc!hbz?pj9X^ce zv9&|;(#!?RQY#fA5A`F|79_;r0(j{RjW<#|oJZH76}?+p01xE)r!ZHw5J9VmVY zDk$&PjR!oQ`5N(QD4uF`yUtrhV)X}oP&^OwffXWXi*E+#zwm+k@_ez2y;UaUA!=aA7}*4;qZ1KjA3@dQ7+87R{>& z5^uX&v6vm7Uv32UiN|izj94J`;^Ws<*uwiHPq*5q%IS9DN}Lu!KIz2Q^EBG4Tbx+p zXa8?JdjF7=1%J}D;6>F$9tZWyUtexG&pCN^tj=#2ZGAWqI@tzO|Gsl>=%mQ9C8_?L&#)}nsGoatC-lqK zH47@lMDeTMjZhNFvQ*7`0PZK?{RcbDsgV(bYAn}&h_}Zn89DU*`^7H$FkariqI2en zbB7u5{e(~UC447~>WQW=sdS4~*;d|ICHO-)Z_J`CjAQR_>&gidE872eAB#K^wK5U@ zN{D&NWiEc88`pIu0p+hDkz45Giqw5Me^xP2J+nIa#)KPbNx9V~#6JW+o^yB@jn1Xt zi&=dI)#HosYJe|6JTBM=cnH+j-OV^e%OxwV=%b?jtggT{Yo)oY5L;9=C-RqGz9F5s z{xX$~6L*TBpO-}XdI$9@myA4-UpAXtMpLKZKp!WR*nG|n#qR`SIE_`l@u^gC*M)5L z_kbtxOn*)`TiLGiLHa9LUA1ORuJP)IXk|9Htr_&ndxG3&!;j^VTx$6}LvaQw7l~KD z2|_51zxJ&|{gMLWT1ndek7Z&F?=n$7ukRwxak4sQF@0nc#8c`$?7$>Dzp2{;bNxQG z2%jN1F@G|ytuMHk8YtOtdh{;3?~6+$uG&$aT?KvE)*^g0%7@7ool`r}`3=#3;pyu? zq)@Nq1ojBxor9s!$71x-N;3r3I^DnPN7peHFy|cUfC+L~h{rJc)YD0YS z-GHx^B;9*0Efco`;U(0~@jAbQZ$5BoQO$lc2>9|t>VfcU^iL@nzz+v}z*8;TjzQ8^ zS?~TqzkU;y7sWvs!ci!$!@;=9d ze4>Z#5*|UTd0E_OQT!g|cfyozug)0virXvXCO5iC9o;~+svRG%yO5P|ZZ+D!AQzJV zxl^xP{Jxr5AbZb_q&0OtS)WCwZk7SO2>SOUwSWGkM)w?e5D$6{HN!jpPp^SGGP&}u z!!1ka&Kc03!0M(bT%E?=4BK?|UQzr$ogZK)+Rsal#5{Ec9kX-0gHiskM=Vg^g^!Cg z@Zvl|^+(|Ecj)%6jKBSd@I3N&!lEyeefVoqe&v@eCcJ^aaBGp+(@E84?&#A+s2&#L zS+U=ttIvDPao-74KNDQZ@$9EX+&y1__#Te|KfqbIk^k7#zSiJ2tj91Vh+#2hdpXU2 zk5sB?9zEy%DrIn15UK|IJ^ZxR3a606d|Oa`RbUUbCE`UA_w&p1+Uc zs@crQ9ga=ETQ`U5Epr)+a@lxN_HByQ8`NK0ZNX*XaqqqFtgrSC8@RJWcUHs~rsQmY zKPRe>-o#ntExIFBk&bhR^&O`KzTsBNvW$p#ZX zEa5AaJPStmpM68#uB&MgWA5{t`0U-Ez<#~;47UG!v$)GS$0*s@)mgLpBD^IeSiCthf&1*?2bG<)_@bm4T#6x7V2hAUaEK0C6z=H@xOc#BGmTS z3>062URmc6(vX~*ZMk>VN^-q>N1=#sB;~lv8RktsO(m7 zUqjYu;-&X~kIl$Vzm51Mg7g;YY=5g#v6y*lH19^=LN1uw%&|=Phjkw1!}`GiBzpET zSFImEtsC+!(@}q^Y!=dQnRXA0irsNQ3YD{of3jcXp6zxcb#<|5cbqPWubZxWv`pV{rV{jQB^CgN{SVGHv|P(2;= z&wGiTHT&SOzdQUB;-`Mh!t;a?v7~mc(@`AdOCerHoliQN;_AP5nKblGro0BOyJ7Xn z1NI-w{6zlyM(AdBbr7Ihp%LEZ0rucw430$Jytdng{ixBDZ0dID@Gqo~WcI;h4Am2B z3m(*b8{bG21T0mCcx=4dOWeU}2`Qqu-n$IepI@t|Inae$QTN@x9_$~)BZWBH5z1PH z_GMDwk6@lcL1BKvzN3zd6&kieJ%ul%wpdcL<4Ucvh5H$`xGT3mIe!^iMXtu^i69suyO2>od45X2w$BcJ5KAA>&RvNTJS!E&I{l<7F^wO z0`~aTl7_gU+9NJ4n-KNDcUkE?!qeFww+Pz(M2=enoe7&LWX3L$aik`Nrjp)3F=HzTz;1Pk-kF$#uuTN;aibwV-Bw)eZlHR}mwb8}~be=IM2}9!1u8rKH z8;Yp^$KYVD(j+&;{!e$qB`6=MdllGeufCDd5*_*72H}k^97|1uETz-y=PX2cZ}5$5 z`j+=RAL;CP=$GH0LZ<@16llwe1GZIjqAl;cW#0{xvW~fN6zsXgE0o*A%5@0UNS*n zV#p+QCi^tR?=uG6g_6ShZf(th32FFy4t9g~+F|`)TIqearA7Vzx*g_5++O*P+`0wi zF9al2^@{xm9)wxs|AX>zLS+5I-tC7s1b+OvS%l|D4w^ruFU~jGvTNhrHSzmgwVlf$ z{x>-FcP?$qfO%4uM-uS&8?t^Ir7jiWTY=rATU$7C@9y?nRIT-A+`Dvm-Tb8gldVmU z9wNO`^4SG^eZ9Hz?e%lKQe-b{zPwOjY*~(POd2rkJLH7)%)QgoIj>tKcfkJ62&y%4 z#VwZCm2RKsA$%>!%h{epk}g_2Lm-ygmwQkn8 zZhT7o)XwwIb+dIp!d?H9whI)ZoR~Gsk-e%ld%1`nBTwMiF`ptl#=wag7UShw>%E~( z)URW3cg*H*>T)Tc(O*_5z9Be8l=%_2Xm*^L`3uFLgxyRVev3TUUp^jsdowygBB4GdHBAfj$cTc=?2o;y1c*;Tp9l|x&7Le! zIMajp8S;OYL|nIo=GdotfuWad2C`Q!HWImZt^BKCFZtC4x+~WI)v%ECon-~{AR5_) zb{>z2&!2dD57j_^40x$kdwa_`FSTzC(#yt#D!I}r_&3adqm$)C@dTgy_))8+O`7|d z0Qg-`X*AGp74SpV>@kWV^)71$j6La?QbAFt8VGk?(I7u9wvA}JjOj%_u6uQ4vLQn zX{k2D9#37kft3F4jnnb)zwigPeFZxx*0GHVcw@yDC3@#ZZH32w6WUfld}WGHE$oO} zV^w@nQpCs6Wjch|I@fizUZsot-EMqi&-^0gr0-gjaNqcP-AoD2&Q&ETuBsj)e8^Kg z-?~!vg@@DhNzwgsmcwzavi7t{YgHjU0rbPo{R7f3T>ip>`~vv&Kpn8@6hFW^KL+{M z&R*=Hm*QYY;yIQLqX@}F!KI=MQ5Pi0sWtWxe;+sHZJ!$ zp$GX7;AgLJYV@Z@2Uvl^TKK*mgS|JHC3M?c{B%Y&*hA=l{=ue+^{pRU6PuF#6y!x` z{%opCiqpVLI_1Op9ATfOQE}Sch9RE}EJX2JpRZ+w?n(A9*>3GU+sXBJf6mZ_!qEL5 ze;xA%do;p^`GHeg!&Qkseuuz5{v3K6(#{)rT^3W zlYh40UFnVT3CwvH+d*SvF7DimP!F;DCYf)BV<$1P!Q_#F1RGC?7c+>Kt(97@eBb=q z4Ci|Y`ZbG+h9hnyHZsQ`A8Tcsnc0)o<8JlQG+lvjZsd&SfdihQVDYvP?HA~a8a|gA zsI7`we*)pp0+_#MM_9H0!!_&T!v3@FnMIK>FT>zmWY@*>(9dk?%-sd`H;nBy@a`z8 zhX~U-ZGG>)OZ9zVd5YdQh-CY8>*eVChHLX1w#v@|zl5^1^>|j|J&r8$f7NfBEIYTP zs2YVWs}bq_6ZsVqVSzKatoQg*l)vnjT%yu`ySs1t)_IiA>bdaS%F6!DykDA`3Hv=q zI2$-+r!%<2E?~$X=B)y(9Xyej7Pj5AC+a)kv%O@C@DCqO>|N@A{90m>33L%Fdd_rx9IG{&|R z=q`?vuL~LSUN)Rd%w((qej^r|R68(J^(CSE6wFH;oLL}GTAlT78xey4h{0LcdBOZ` z1MY;+-Z_l7C=Bvh#iBpfO(Q(Rpk@it=AFfo3TG{_C-z@M3YsiiSYl#^yx?ib2U@W} zW><*q`q;@oL###lbfk5`qS$A(Z7MD&Q9doCdb*Mr9r>z-6H-O9xmNc}ddgA!*wLIj zi9WwJCdl73Z|!9ZmFod}R(kJu?g#{aF;;uicR zjY%snCv!hRq3j7B{M~K{he3ao%KPjWFeQ)H1E^QlZ(ScNV7iA|! zJT7)qOP+~*!)Xn#DTvR#cj!Lfk$wm3X}Gd~1CyHfML@woBEkvTi@9jd?@Oc?PYRBJ zK7l2~8+)Wjds$lc z;@1ZZN^PLtNl@+Ja)s&7QlaG!{M#@V*krH1GurTu?iUQjo8(tC^AY;5>!fF@jfwvM z(@&R;>6vuCjPPAdC8oT*yT`2WG@%T|C-!YG9|K>YR)5V(d9Z)O1R6)Z*@CNH9kZ?r z#b3h4T`_So4|kZ^6%WIG>0!T=#MLY~-mF-wH3t2mtwg?0p|hq8c&Xq`i)j5DRVHx59NjLFbuPO7*G)Kiq;w> ze-^*#&RP9w{*}<2F@@#{D>R>A>(nkW_pzOFD~S^wFZh&&tzeNS1=g8%5PuPd*`7H$ z*|~df`tR9xC+ulv%HB2oSUu$1U?sc|Kd-v5kmleUutL3R1?0c2({)br%<1A}?Ilu) zfEPpkQ%_F9_*9DGuuFd+XoW28e=oykP@OaDxygk(ZbRot6dckqA90m@*F z1$E|L^t^B(YP#6e^L5m^LE-#@Pr;=P5HF6iqi-~oD8|&qt)IPs*3TfFw0lwBQIl|) z{jU*zj_J~hEchHe=iR;zuy5Kkyrmi4|Ijz@uK1FI;$QZ|m+CmX3l9x4Ch@S|y=1+y zQ97aHV4`tV{{P4K9hRSNKRl1(If}C>(UN~Q-{i=}T6BMd9$K6j(O#LZEM`85;vvw> z+kTDda*Nv=%vrNI?~zBFvp?d4<$dT}ndtw&e#K$ET9#)j(qj#}VNIvw`qo~TX8FS3 zw^E?L+-X3C&U%07cj6`6|Ka&(6_4w&fPgCj=Bt zpVI>T82p>0(&o;wE%qVRY~UB6erE?EkNL>uam8-rKk9w1;e`HSvPVFa!G_vv)*nyhxT-;}ee(zwnO=>fKS1EPGA71422;_wFk zUzu@ee}#MFzva+}Gix?yS|R_WU--suzpi3gSo4$nHnCv;&!pP<`88+$vegKS>WO*! z_0d}BSK)`A=T8wKo-rnMez}A6r#>Clf%CGnksSPCoXvhseEla*ZTk{$J!jj>eG6b; zp7}Z-g7^XEmv(ph+rMMoUO)0D;_s6^>ynF$3$>SYUV(VBby{GdnM2~rSN&_xd=K&G z2*FX&&aRoLIr;gpUv`N|?~UZx&S5N~mrzkYrQaMcCuh29Z9FFm_zb|WHPe=rm2W81 z`1&(z!iqJIvGba7N=FavKCNQ}zt1OMne5xA^Zr=cXB`#MyqhxzWJu&!53;3nPa7V? z^&&h{{ihh?qKk?O`1=t8%p2;w^am&P^Z+KRPX#uO_)|CPvljZ{{=@TDYf6lARsZ(9 zv*CfrzelSL%BpYPDotGT`R-yX-~&4*BR*9BN_mLic0#-zuhI28`7pkuR1Nqwpa&zB zou9Dg;l-9qW}QT69+nS^Cu`HJEwvN=RUMX4fcyc~(;bCfagEIAQ`(!6 zKQRdGp7f;e+iKUe{d2bZN9+(rCs{x0Y<3dP8=`op+NFil(IKhTlTW^)}Y7_|H0|7^bx4YPSHfyRBSV^dj;MUEhLx1N2Pq2C8-WPY2Mf;Dd(0+G`6VK0l z=NYCL8&kJ^2=Pa-RyERLv|b^vEb?$UDVs?v?!)?nzF^@STWUn#ffd*RA5ncW7~Mxki!dTE_u~z{d8V^IpBnnxni)T+H*@r2|{C4rgp~&R8fB zh>P`qC7(k37v#(s4kkTMNSr#Uiu^z3$YdRvI~5J{d%PR^{{D*kze1c|1vkCMmfC?ftm7>)!~3!f#W@jv>7~wlhP*Cirs4(!tV8h)*wccT`+V zlh?8Oa>*K!P&xq;P!v>hD$RZ6`ctBkJ?D{*UiZb(qgxt6c2D`_k_)`TGI5j-%|-h*O z(#%90T7RLlaA9vvM|%44wYNn2?z&#Wfq_42Q@@xAz#m|QYRw$WX4VTW^6{;EW@BD? z9U2C`OF?Lm9_1LqU)U+uQ-nB|%zE|Ram)VkCJl7FuonXV?cso~4w?r<*ha&3b>CH( z-pqJ}))VxsEOKPhY9}oBoq+Stp9=`IPuh}|I=n2+4({(TrefFLa%`<@_g7buzI<&e z=s`4_0iS=*2gq-RLWtDu^70MQ8FXSr_U_LZ4{HvvAaaP}q>&4#9xM!G=5{<;`O0t8 z3T4BGEu#5sk{3>$PkcRrp?u81rEY&c^ZkYl``wACzkuL`*B!W+Dc5^|*R)JF?>S9F zPXqPKdGN17{t5Y_`?rzF(v4Oj3!gy09Q-{ArfcVz?uptM2tPOTh1HrrE$P>ryvM!L zV1J>WF0SQdMXLCt{Fw}#=V7+<8DsVMWhHGU_!o))9Qco4czUT%5=b_)a{${lFPc%P8 zK*Dns@gT1&^;`k!F+2kBK}cNUg-rabRJ5Kj4~}$fz(wtTtZQ3aJLqxkwi}665~y=K z_h`cXV5-{c&zr?r@f+Al5}Vi3?pH_Gv1hJ< z{W?Z~`}Qlulc9aIyG@t(uFAZPhUSaU1znk4OSR?xGK1$GA%I>(*Ab#Z2F=tS<&SJL zn)wvBJz?@h-2m9T5kildQqE=t){?P~P{Ymtq@#Rd-;SNHr*-dv{eyg_5LbS6_p*}4 zCXEy5?B-)6FHmdKRi<$EW$Yoz=u6OU2Qo<4{=$3{fO^U$665JfbY$&Ty6sP zlwYmqd(N2pK*!WoiIQFQ0XsG`#0i{g7q=nN@bEn4U)isSOq$qWVRFhZxF18cwbbpy zHLuuJxs|DgXIeZFUZa~e(fXT{#ksQH|KR%){$nB3qlg2foi>(merr?|>VC-wod|rU z2>HVZCFVvkeksg~k7?z?dcgc+mNebSjSzb7Z*{=iKyPP^As4?p7y@2trgmV0Ml9O%8(JMT-qnEkpJ98XC!^VLDCKOIa<5Fb}Bwfo}%^%32a z&&R$2UztaE%fywlz8VmIB^nMlCJ^tpyoGs<*h14=onkoOy#$vNCp?LZUY=Gpm}mD|YjiRhKaV#hWvFg-mtz@O$^?oNx6){b(k{^rxWQY;|_gr+)vcFOi04-mJOw zaV`Y=`kux{`NcGk9sQFA6`d}wEz)0QM{91UCw$GCyFL!}PKeh66N;@Y-L@%CJv#{b z>Ig-(iOcQs(Bmmr+!paY9!@tYX-2P|T|{`@kg-$bk4UcqUcYL6C@uZ@>xt7UKB41+ z(nFhmeb|KXadJ?6#5e5d^5YqYQsMjmo*AzUqK#rQU-JtJ;6C&c^xMse&Hm9fJ?VF0 z{vkW}$CdNoPeY0Q8NiQ#`qE3LCF8n9>T$8U8hGDneH^1+p2^~*>D`3*1?n*eJIc0? z+$mKV!13Y!JTGE+${>3>X%IgwN{-dG_~}wNb6@x2T|c9%0)X*Ifm73_>oHQXAguJVg3X20H+~ zApDk$+I8k{+m1ky{zc>mHE+l#c8px6 z@GTdWZ8Lcf-}jtfOVy<2h#fVwIHwnC69f56EJu0OfU2i&$xCTmPE~wfZvn=h*=+ce zk8S$@{7n;S{;4rIj}IFu1s0a1iesvl{*U&R^q? zqkfK%UHdI79&{ux?z~zH_LwJh5z?#XWyL($EvX$UMe#)kZ&TQUpVyQxe{``a2Yac; zNbNh2{EipeQ=h_5>#UMXy{WJK7lDo$c@5^ev-ujsLUB%M}`@IT58mWxBj(Ut|~ z3emjqy679fy1OFQF9f>!fxf|bPz5b4Jp9A`xKFxC(9g#Meuu5S|IO6fr^&TY@Aj_2 znk(>Ky1Cy*Qj+24A5r76s+&jsJxdtHRp|NHv!(>Mi>DOb-$!y#{2>f_>PcN8n;BG^ zg8XkIJ4Um>WQ@1AzBCrqGhm)~B(3;pdRlugvqg8q9QFR5+=W&_Q20{f#A2u?j@MTF zc($|h-657->S7e1=*5sp^esJYOAhUjyek&{@3w?jJHO3@P+V9J{42BWyHjsNJwp&0 zTF}l}92V!-#@L+&*#Q(X$fQNHv$R)*_~6%~el7E6w?qdLbp9hfA;pY|lP!;C8?V%4 zGiLAo5X=j0$-f1pqs5el8N+ma3#CwR{vOK9Ur~Ffh z?JpBL79spe5LDp%SfWRA=UmwZ#CIV$+PS&8t=02W_7tFgQu0lU?Cq>B>bq-NLddr{ zgteZ=Th-jFg`WnEfDhKk{vv_H?ajE)U!w*0qjN?-pm20yf8hz~V+XXeAAbb7+3{#v zGZY~U3ToCUlbgNl3t|3>pq@e7eAPXDcSd&2?I%0|8|#O+Li0pwgE=g`#{02KWsY~) z?`wbV-Hx&)45NkeaHX(6bF_ldHW!(jb?>9ZP<}+=aH-1A*KV4WpYuTcd7-Cpn2wj! z77uj-JfxW)ls@u^MvJYeygR~ki|~N+g$ZkNEc{)G?0JU4WjK#K0t;_J+AY56Oy9Fq#HXwK#tCuS z%Ddu!H8KQ$KPrJ&l#8`aJ(BCM{4h9c0`nI-R=WYdQ)f3BVz6u?`^m~l;%n&7CnweF zN@VYOk7Z#qo(rRS;bM}sd0p3@aJR#`ScyQ!_I>3hF|aQeqh~TxH*1#2{ru3Litfu? zH@=429*c~$;F4Bn^{%-e2lfa0S#RV$w@hoZ zsO+YJy$3zLXE1LoD%GRkkpiDTUNm#A@CJ2nQkV-e zSAO4n#A{tE;6r|Px&Ba!jX#`kPXd-f_qcG|#}9LJdTI81ciE%!F1XpWId5~byop#M z#Gi1UUCB_7wS1nNsNJ)!F%Kf6UhBk5K$O_rfvhdSt46^45dW zC_Z4{c+*@FG5E4ZII}ss^Bp$c>Kg6StSKWXG%RR?e$Kj}NjFWg&oxy4##pq*W=qq;7M-yyu-C%`;*F290^bTP{24o>FIemT)5y}&IV~|l zV{XS_k&=5FhyaM0=UkLKE=_z% z70v&CP5AA;PgiV_$lh7VbzJ2RCH;LjeiHTfP^xV!%ARkEuYO0?JWwcF=xe9H>0RD$z6UF&Ssdw9~kGIiUWCgkf- zPomK@oQ_>rZl_I7oM(dm{52&{H@szEfd7`nF023emcv6Ah7OkjKemyer(#DEGLGYT z@BbFnlW*AhTGD6kraaqN&<61W!6~xI;jN3pJXNk6@~1gUm1fa1`|6a?QI;pvx1K*_ z2AzlVM5cF6T}6D)kPIP-AyIaG>E-vTvqd!9fl_DkAmsC^D%<8zyh-*lI$`_!1@m>U zg%85Z3270yuF(5gy@zGIcYq$0fy>;7p&{z(`_I(Mugu0kfB3g+@cCCQ>3j;xr-Lk> z+R|mCZtmQ7&>z_kVJgnnpU+<6+Li|KQ1g($%V2xa_J8&7UQ?@peri)^zn))0z{3yt zSLh-=OM}LQ7+Ss2a`FDa%0#Z!fBT`BA0@5M%hrC6m5jUUjzmaoGRk&zh5Ot?F@tic z{c{Te-(hv~>`IN{aG7cwS*Vvd{_O0uC+)z!gL#+e{USkc zg3%z=D0Ww@jSlDWro2RPw<>*djY2z$$MsWb#xGbBNv9pmCgJ|~5&{a!Q*Q(_$*nqVNzt@w)B^3VI;rBV~wuo;P#IRFQ;N6Xr zyDTZf`&Oya%6^gJ`=}*Q-&F4OGSHs>eA7Yu)ydw8cV7Ib zxA}Sdp(j7Li10SxhaHi*ry33OiXa|MnjISQG+X3EY7Ymh9@xJ+XNGz6RO}WdizKN3 zfgXygbqhn$e7V>eN-Fph9vk%NvWud%YuPQgVIF8JffioQvpQRCHP&Vc{1a0rF7THi z-gwyEqAj|wtUyw1ScmlF@(n?VPojVLQD27zpK?f12ka5#A2)N=)YZh>QY%nDXk{bY z%gx#MTyw;ub71vAVy8Q1$go~8AFaA#rM&#V_1*z9@Uwa!<;9yuDp8@3 zsO-cmYRstl`e?)p`TzD#Qas9s3DE|2{+&PA`1Y7$n^y4m!3NS|+1??Q{rSfc9;feU zJ*AL+bWeWo>$aZnmdeWx<1NgSR7!mZjxvd&Kocr%ke-PIg+O_-IleK{`_}{k&I6B900LrSU95OUkR;r% zK7!Z4lX6@)>oz^!1mcrksAmlgiQkQV;!I}%{=_4k<9E=C234tHpLu}iv=Sm6J3IFd zvL+W&7NdR){k3)`=>)#G>OFtQ7+hpAs^s-LjMJxu1=NBj;|DAJ1}}9 zL7%EYa!O%TU86sN_j@I*C}h_!@N(QT)VdKL8`Fh>>P{t$`A4_r0l&epgSkHku1HiT zz6o41Q3m-Yn&;8@xbpI}O-_+cfPr&oDw-c%D?hSVQpBGLboBjj?L{Az??CApjxb6u z={bVtPe(S$F&me7*Sog`i1J7Arv;T}$NJ3u=~^THP~Rax4tTo))pIe^k975l=~9Bg74ul-w)bRb#ntH{ z$UkrfVu)cL%Vu8)8#9;q<6jrzY zNU^z&_dux@s=s5Zxt(_Qr*6&D2j;`X?>W>%{{|1kD9_Z0p78x&VIFeCs2WXz5v?^~ zBLjcGhv+Zl4_xWL69Ms6$q=T9*L=(wiZrPtV`~K-hWuLP~~ujb=qzyc6xlS&e`>G zz@Cu|Qf9F;@RNk(gu$f4ZFIo@Oba{sq(9^Yb6Ob6uQ1(&f1F4>-4)EAxTxWn`oa>u zV$hEd-fYDu7oGMtaPBhQiO%yuPuIl|FY{{yC0E$}`ghhMvs7*wniq&4fp|y|`pAlZ z75OWH`^g-7r<4B3pL>rZ{8!IXGp5*)r)*^x3H9MaeEmo~2O9E0ul8a;@+bNkL9TT= ztCovvkS2?k-F~vl-{m^=a|D0LIzfVX^A+KsS-X9wnQkVALH|DXy$AU9x_#!Bw-3l{ zMD?oLf}h5qZ>Be4T>m-oHt9eA-MH7Uzh)5Oe<3N3Q(d`7T2;a#09msBp+bS@kI9$~s6e)XlNqe6oYiuy9q=QZxy>(I??^|n|nX2XK-e-V1_obl8x z^A6odS3rN_FzHnYf1LazYdYh6QUyAzQ6}qfGiTGDBnncpTVS)9a z$76)&2^A%>i#$GeNMAS^0eI37)L)8fXx%vmIR!Onz1XG^I4h?O>fJdAF9$s%U3^|1 zV^JHc!V1~{9(KB%qZ8LSwtA1=9TeXYqU5QZ59VE5R!9iqhftyo%HO;`up{%gRX_NL z9*PUqj?}#KX5ihm_u#zr5<n6K$%lFA3$TxQOupJ|d9k}9h^XssF5#Pd5H#tG$)!yWj zY5r?wGkU3-y~%vAM>J~lC4_GXxlCpZ?UuX8p|9f5k7%5(IU?%km`Uyhdr5zb={DJ^ zGp?~%Y_b6189{*(v?hMv&0UR)ChnT}nyylS{fBuwf*KwCGTE$G#pk!n{J;bYo15Ty z!9TNCNjf;Wt&8Q_Y=HU8m}M1qDpC!u%)^}b?@>Gu0&l;S{;}4>wQC8S=UxIyb!$QD z0d1CNtnlt!?%oIVVZd926g6A=4BU4PB@*<})YoOlFN{I{{+2&?_I3r4K8Ymywhx&__0+H>*de5WNiPx7uq@cgapAn}L=#+q91fk38+Um3KS z=A>@?flC*r^YER98zW1VhN?} zq}?b!qc}3{IyuoM^OeGWbUy?t&vrWrNgF@dW)KmcK-p#8+1YMt;ukOqd{w9yTc}*i zd;Yvp6PvyR_ZjqgfbYj|NoZMnpAY*zLUDDoC611l2>V^dkw2;p20Z}H)`WrSEB5gB z!xSeAMtmNzOl@GAh4^ypqLxJw#cj;*Ek{})zw0ruOb7UzF7=?7dJpdROh^GO&&rZ_ zaBs2E>Z-<=mwC({#K*eYJjEwht!GqkP3)8SA)FzD=<017U$9Tnni_c8`wMqI6&At$ z8)ol;&XwY@jB;BAK71bZ`zGLWa=i6JKRsT7;un~QLTj9I&f>aW5J=<&)vWEK*0oMa z*+`ftL2J|5Du>CJ=jLXvUX6Kz`c1%oIa;V&Hm^OHq%zu%;*;u^w5BG_J84(VWxT7F zXX$A=$8&-B6njteDEibW2sQfreQ_yYOOFh#eC!x<4* zQt|f2Y*73efG>ad3&``GFDsB9iy%YOflOY?p6Ks8v3;fKKBq3ll}Nv-nahOw%C<(z zK}DVKm)Z}r&WwS7E@`AZeB;6ScHP1nn`1+~$FB_-d`$~`wXBD^Lf(2uIK^#nI2~XyK@I zeX`=|{Bah_$Mj=wutG_gPQo4C6IWD72c0@+5TCowuYX?|x-TJTn7@uiyvtVU`|bqm z(Sup|`i~m(SdR|3P9lCO@QW?%kG22RzIGFw?}IZTUIW_F8W%I~OqpM>(OJOs(-J`b zXw@E-cP|Iw{sX?y#F34=ryOR-^D{ij?1gz|l*LN4->+RRlXgJ9OAa2Rm+nYSOZP~> zBf5`X-k`U~*;A(MI*t54<#LRL<$Bdt7EB^S{2-xjVdGWO_(ip3T~84nt^1PB*maHB zAYEh#`fJ4A9la<&3Tf)xtqX%HtJ3i?nFF9tO}eKc-|+si`)9!GeA!}`cU<;oBRmR= zUNTB1^`>|}jr%3Kza&W?^kWbadpm+|EV) zY$ZMH;E_{X>1}%+y&F8y({sf%y(E1-%Fnip?viit_-E}D5A+ylHuJf^UtisU_->}S zA|3u83T0bLxbQQFz1OXbrg1-`{OP)C#3y8%ealg+V3sM4RT<<%F{l@{VxCkS z$?56Uv@a|*@He0zPW2P+at%B|GN|h_V7FKksl|sG&`~k*x(%aSS@@r`U z=)973BbO1UR9{KezdsJoJH$3)HqKzZvaJ5vYSHYg`0xz#tLiHno9@OLI-+$(PG4la`d+Te-P0wj=wi0e{Fx2ZI zK01>NdXT3!u#U@3??C;o8SywLtCsusi>Q^Qzz5TBd`8VNdix;CJoP%#?}B;?t^EJ- z^yTqT@8AEHQYhV9QtDPxwy||fglcFtGb!5;B`wGjT|{ZpXrZi0mQdDc7K6kHF_ji$ zQnrzO(rqEh5@VUh`*&V*Ki}V<9?ARtel2Htp67XzRh~ z+aF+mC!oK&{HoN?X{E2v43ORt-dLvXSa;{$xVOUCg&Q-rwHi9NeSvyE@8fac_W(c0 z6Z-qwS7nqtFDVCqG!nMp(`mU(Tb*;V<^u@t5vSy4TMb4V$ycD>IPb)u;zg^zPMtWV;uAJE6}$S^N58eef1H7SmrakYOOj)+m$hE1fb#@=P9L6bx$;s4=TxMZ0QM~3`w`=%rt9qM zvzwrv!YS{g(HQRn*RMOdYxIoqEFQx!v0FmH?SEljR~y1VuGN%C|E z*pOceDV~~n58|^A`R{qXaTAKyd~C<;dD|BR*{pa1c$;uXRKVl1LYk58>Ce(|p6kPK z;(u>(>Q&or-<5}WLV)j(3D*ldR$YF0$)eFR;A>>@jw;D0sZ?%1?T_-)Fvr)jy3D_d zqcV2QNA;Dk&V2hv>Pk)8>t4u!euW6jk`2%P)ys~{>s^8FXDB5bIMc74JeP-cmOU2QDo(qwFIfk7d*G^QM5B0z1si`S~@rFju z=Lb=D(f*k4>wCLV@wV#IKlLLHH(%v^{#)V9xJZn*ywbl3;$^_^y-my2t&;t}3a#Pi zNpp641CCp4dHI2LJgRSql5slMB6t0vrbsbBd;zHcG?Gs3J|-~a`fZ!e-J7b%1-(i! zW!dDxop669#W$^wFr^MJJ5=+Ti0&hHk!(vE#;2u3o0eRFc}rh6p?Nn@|6!5crj0zR zj<>PT2uH-6=eFdmovWW*ajnb|bAHJrVl&tZn^t&#m$+EHioOD?n*-3#3;mo9ueTuo z_V9bJOg%o1aD3^vAc#+XuXkH}5AjPluOk_>fd%G|x*LkoJZe0<-JY!MP!?Af0Q{== zFrPFmY;Rn_kiY+?rLdk{s*Aq<-r>Bci=h=k2ruT`u=^)q%rbFLD;wgyLz6QF?SWga zB__<1%7XI(eE*BAHZ!{XYWDrF9yc5QDaz3sTm|_lmO~o^e76|pIk$KP#6;uh!9l>M zg!)Q|kVwj3J3nsM1$PPF6@6viPKPzzs2{J_x_B>-JTc&1D;Vk5*9th&gX{@aMM$t` z&dVtGDHfvmcSn?dc7w0%TKDJ5fKMC(yqlT5=p<0aq1CUlhoVc=lsFH zelw4;w6J`B%dCG{VLilaT)d+?owQ*MPl#2jHT^>I6WD7f>8SG0>+U^UOX@`W!|P|K zT+Ta@HNgHte%8YC{dM={?aAJBRBsLQ^m(q!?4pkLC2)BTB6_5 zHSAZh$o-_H*5_Tc>BQQCB82CNF6VrAT?F=|o6|MZQbC?F{azo&o3& zqgnBmT#el@EJF8zlTOE3^I7t#7cx@ezK>LU?7AZTXPO0?6aIRB(Ojf}H&LQW zgW{!IcF43hdQ4{6m>zj&4UXCY^6Vd=2v=&Ka=E;+Iy(zM73i z`AJP=NUNS~>&j7iL%O8iw4cpTGg(v3TCX@h8e0YRn%#q=E8eFXcOmb1A`r8C*T`{Iaa-(-}wQ`Am@X!A#rcmb>* z@G16z;1w+ELD2t1&#%tMk$9~QstH$C)}#IzYC7MZS)ZwX{3G>oxYC)kwqNc}LCC@R zRZ#@_G4z)it8;mLOLknLE5x_Zzu~iZ!*1{1GKG){4|M)S9_n%1$D{XIa7vBAew!L=pz zKH!a%+S1KWMqAPR#5(AYT&ps1VG90!aR(mvDvxQTO!~Qw=;yNfLcslP>F{wfnb;2R zgqs8Ljd>$?VcwUB@@hB9XVv#_uI>Y_mwpaiTPL3AmxcHa!<(qnX_pAA`Ob#z3C907 zU*RB)_CgQhgNgx+JkHg9oS=}BrVIAr&6GIm6*ERR|Bt^Dm3tu`NBT0{zjaugIAV%C zoYzkL#plx3T9om_pWW9Ze`*@T>bI^?V*k?>kpTEa5e+xsi}b;`ljpXBe*r!+!^LsR zd0kOP6$|B8l<53J<+!hZ-RDM?LH++fetzM@;x~G~7a%=bEKNs>R;8qwGSb3MxXqpX zZ$4RyRP2E3o%(sfJCE3Zh5lu%rjE)2z75pV8Gic7MhUFZqgDr-4By)^RHhGB01=#6GqV=%b{TZ*U*@69J@)3s*(0?ti ze%C(%^E16}~&Tv^}D5=j^ z@fkQd)wt$sSc2sI`t-y6>z0j58@#(w{|wbP?%AZ%Naex5fdA$L{zqe1PK@r^T`Pt^ zbb);Wy)4bGzx#4kg=+E#!9GAdhGP~zRq6gG>%}tg*ZN$nJAz2!k#{?|f8K=j27SsI zqYkg=P5s*>^Q1+D^|}A_zTP={gc^<2^&xB2eS!LH-7=%6==^eghCxrhsd(V%KFK__%60?(PFm)(6B~8Y z!T-SExUI%zZS2NELL}%*;A3xMr^6U__P?BL?Mdjo;ZMKN;S!v(Z*TM#!1?aP%ye`~ zjs5cXqg2~aJ|AWxqrUie&t?txQOGyozMp;p+&7q)k@sP(BwkF{C)x*_^luh?rX&0> zG}$qzE@t|=b98}(Uqp%Gcj2TC=N|-pFRzvNpSzr;#mqoHHIr#zhV}<|Km(4%nxzmC&?i-UeM!(dK7iP zBfa#3D*g8I8JMSn{W&%AEuyGrVnfD*&krQ~)m|{fFI!t@d2nJ6!dG@UMHb5ZQgmzC zNnU{BznS#>NvCqPsLP+`ldYcQ?#(E?`55x8%WUQNuW-IbIF3p%o@n`-GfoqrHw5+A zH;!$`Fs>3SDxC)V$=$(c42p){J9-@oISD^Q|9G5HMWGzN$xgR)Y41wL{ps6a-(F;W z<7RD_)YqK1mbMu?{9!RP;*azqtE1$Pxw#JN)@(;|k+{)V*vtH5@`48ADz{eU|TLWeA zcgoOt2RzX8Adg$tq)^_kbrJmMq$u*8jt=iEKJLx~d9VjxW{C4yCBHvMcUa*Wh%Zmo z%w;%CedEkNSRx3&e&=x&Oq@bHLb9c7y}KRs=RuFG59*z!q*h5h8F223v+?%y}6Pr#cGz}Fgf{m~Ej>j-v}9L%#? zp5Q*@nuzcV@r6b~VeB8B8TtDT!uNFIojr2oqh6Ttd;1zopkDw>2?-1sR66L-p2|S; z6wT?Kla2J_Hkv7cnoY z;yY;tHJwx50;oTOK5Qc{&v!K^t>^P`RPUsk;93W-)Tglg24OvTlRKg!>dlNAlq&x< z-iz@5Fpuv%U2?bL=(CmX33i14@ozyd+`BR`y9&+sm_}}P^7z@w zmlSj)cr?X~wHY^&rds6g33vqbhh(Kfe~5Nb0sX-dl+TL1t#MXj*|p(>T?T;9yvLid z!k>{JuDF=!=;g(|+Ay$*dv#beeaXJLv#0TqMxi@U@2LJSxa`DRdRSMb} zwRBVVCaidXlfrNab8XISTxGIZ;y*l!3fV^QyxjF)f_=r$Kz|bugrqmdb+yk;?{O{l zYGp%xf2JlbdUxvGmm&Dmu`6QxCMnePf&hF%Apd{-E1C4Nf6WCY68|n2Q+CP!{hZB` z<2RLqH#xQ5cq|6K(nIY@$mco2ntkdrwakY2(Zez25GS3VxInx*t1AEh<`^s}Mw@)cW~)`_L6<-93r$eFL1(;eVOMx9LqWJDHv=ZC|)FHZ*2>%Np+ zxPrA>AYK4| zX9$tYWAth2>|)d+zJ)od*N${;N%l<>r#pw0J@%6wAwGk714BJ*W0e16##b4eP2!4Z zFA8iRKA93b9t>(VDzn?W*K7dcZxlzi81tbE2dL&*~)e8OoOfsk*85>Y_ z=^gQnhijnT0{GU$pWp42k~NRN(5?l3#t86v0s~0*pQZez59a~y7eQQQQ1q3XS*n=6 zayrwV_R|^qpJpgb`WC1+aXRt))S1>=-7DveZp`yC^4sae|8GCE0yqW>n=e_cQ8WR( z55q**hxrIkF%5a4{+J}Y?laP(Me%+J^eYzqsJ@sXN0a*~6=%WU*Y7av&t-DrSZ|4$A4>p#!!!efy1Th-?^Wl) z9+(fp{PbMLgtzLgS))Vfd{8|1G07%!oR6f$08~#ge<(9=Ucgr~KJ_->0j;3-p2K9) z(+*YtWtxKe^*DRgN&HF0nC6vnlahIU9QPtJK%GpMYu8Y-457f4GCR%6cs)AmgIBcoH+!A#DDLS1kjH`X)(rZiR!}B}w zr{8h81nXyh8;@{Md?NB82L$NL^8Wtu)ezy=JJPPywbnZ7&CvFzj>fX5v8)?XInSM0 zLT2^ayBnQ%J0*O5{ACEMsV*zhL-wNPhPp0~Y`d&UuVcq*F*7gkL`+SSVh#BJ z0nAfo{!y`3+*n6?9ICHU89FQ_<$7YQTUXFR2l963mShL8CpEeYX9cKUNb%Nh+`U`+ z^{in9+-JbIE7W<=4ZO=vW3YN#J!5-naXrE#D36Es?L>Gc#j%Kq!@Vqe)y=+*_TN0% z(Oy5kZ{vw)$3l?(q43Hp934M8S0}C73-@WNCO9%2r*u47|KK^u=kYU`iaL+{e2wAU z=RHyvHU_`)X4IqBG zc=_CWtzxLhY0LyBjQ^kgYq5%d8yjY^PFEH6vo4rWrqda63G7M*B z5T1m0e%{b>u6Ogs;%668zKESKVocx}al*Sx;Jmz_td3f&J~X~o`G<3*-+y?fFx!r@ zj`M^27v$sS0~}*V7Nf0@Kq+0m4%K6*Dh6#Wlece`I~M~U2YeDAXVl2?Xr3rVOq_*9>m+QUM_=PvvZb9 zDifm00q-5*RI=253DP;YrR8D)-xKagovY0q{IGJkyXrW~|F9@&!UuyjZ@gYlOZE@* zByM5yY`;|(3A!YFgUCqTLc9KG1JlP#HX%LES43u6t-Hg)k7-vvrCpgT9+CRv zne^K23FZIC4_;YOZMff{3G62&l>YmY{+cC4+Rx;{-o2+rg!~FRbl_#2=bL3Hp2E&A zwrf}DNeLdPnS=8P^B%9_a*y_=mM1VfkUb6kTEqZ7tm4TGlcgvgr3B|bNIFVX-?+}g z5YF=;@b!1%aEz>WwOi%1J$}27G@D53eLmh4eU$%vd*iEV?dD_C-+WuyrJwC_>P8Co zeq`tIXV2m>gdae>q{!Tys1(-}nSlK5jw3aLL(Fo;+PiVL0Ur@?Dje-eT-h?~o(msF z?GfLjZfv`X3a;e%n;^8G)P1E%Z^^}@>(`#!3+s(xDl$fn_TKA$wya79Jw5@R8d?6~{lA>_jl zFKNcxk=zD3vp;x{pF_NyJ{hRHVa36HpDhl8{T5K8^_enC#l`L9VI>rgz1{hugYbnZ@k56E6}6_d^ad5byYpSi zPWCG%&^#`jE$fUV-@I{^uFY6mdt%wPo8X_mQG#WpB8Li#JN@`zuK)Rrc-o!q+!V?A&}3vsPYBp)cNU9L z{uXw=sKI{GYt|FXbz*e?#82|a+b+IXoATM%ugMYCgX}ZJ-dYKO_--YXTzee*^Mx741jNUy zjKFE6_9W~fXuVv9;-ekW`5GkhePioCZ%i(ncVhR^JH{`8WLK*mATWaW7gn|(WJd4q zHE4FpQvf~S_tc9yN*ssGZiBEBc**^Dgzd*R;M= z80ckkU+pzc^y78YrWr+FO9o?5K9_TXQj7Q#qTrnddWF}O{rtq$sJ^{ppYYd|Y_sBp z=Pn+gPx)k^gb)Ms$uH@&23W#*|6)%0Q+l4atPXh{!_<3gaAoPH`H(Mwe_$za_pV!) z*y(s2;n|de7t+#F^VWsi=4zvQs5qs-z^K8o&HJ2U?dTmY!awU>%#?OcaMMyzzb41i zCzv8`e&Dy7FplsR&a0xbwv?i+I}0t1!Jk0?Mv)blo4?K?akD@2NBD+GzR}f`6tB8V z3ABl%M}5$51^kV$A=kdAz~>?zp1P4%&@%V?>p-O`x*q^7In0rruado(Li9diQS%Fp z(|0YL(*wQnyD_DuZ_=?o48x*k&N5)Xrp#ac#A*#39o_d$xZ9M2{fcO3=5)&5hyKpzARfJxYtd@V6f`Z+@33~c{qwz4C-F~3kX8Ts>Q68{|ZhfjqT zh#Ck=wD(#3dc#zxq`_Z(B!v)K=G&*NW2qj;Y3E9MGp9IA-?a((&JVegIi9uE{E@#t5ITLTT#4J;~2<%@W6sS zdBzm{o0HJoL!H%gF8Pz%gBMg(Kf^AH88oS$0zaw5QS|;IZ~gTx$Lcc6)GPf42GR+= zC5Ow=Jmc@z+V~})XODSzHs#8kt?sJd=Z*AAYtr*)YinOF>KitOzYpviAZm;hD)EDA}o17CVy+p;sno1Gz29VPIW_Nnj5esKguwvDq~3j`;`3Hy(H4KeT^ui9WE6@b9QJ_!F2X5s53C<}0uo zE<_ouQB7YHnv2ehU$=3c5jt~3nkO0OA)Xlh`eCZ_Cytrp{yCq#3G_3=T)1uR4u^`HI~V$azJ^5K ze(`bH-*?{rz9)%~FZ%7?Fy!iOe=l;P!^p$Z)$RuBkEuS}+#`$X0ThD1-DT$6TGkzP z`NMB6<4-S%y4MH&*)?bEwr7|1!#p?Ng0a?Lx=xJwpP#MWEYYJWZg}(dor{_}if_$> zg)K(L7E*=7q9D{ik2hNk^FMB%P3_V4ORz5bA0C=e@QB$hME<-o)Otg9(*m7@rRL)n z^aJ&?fREbDzIyxKsSwtyxD)S4Bl*m%rDt$1=N z^CrSSL{LwNj9j#do*_P1g7U|G2f4h)Tajw%MgqyaWJA*fs2=O>4Sab!HbV}q!s#oaatUXTuhXrZTymj&TFL)KQ^||V<<3V>3~|J>i9%4hCQ@6 z;)D?Oj?qNx6-76L#zf*BAZgr>__TPR^-d;_`}RfY&jS7n_HD&x%H)*gi&FM5oOhEM z%#po$`K^U}S~+16QKY(K9uWS=EkLSZ) zGy7IAvMpn1dOG^`M?-BtA@=){nf8UtC_W3V=vt**sE}1+^#|O4sIQav8Ll9^npX?g z)gym3^D|eR{tN2yxO$QF z+Rv-I?__U7^Bu3n^X&;ODi(7Lq~|YAD`y)ovS`E3k|h2{bhq(XQtq03{iOw;0j~kR z>>zQ;@?#}u-IuwSp!!zm)?_86S`P&~xjpE7gu?Tozaz;?!JrB7EB5i`FA7LMhOB8S zdto4PKdmtII_mGjwN6rLJ<(595-#5BhkgiQmI&4l=B1{wSaFM0vB0j4QY)tuHzyaG zmiLN8VN6|e*g?Tm@?YO0YDX|x9Bbo@0OdELkdqs>x>`m3u!=5`#5VM-%8$;vn7s$eH*XGk_Bs%8wM-Y3?+;K9 z`xLe|FMA!BFY3Nf349g9&Lre-BZJ>-B0dHuMV;B!_v3H3AU+ZB=J)uA3WY`#{Ut(s zw_*7IT#irvsUe%wzh)InPztp@-NgI?TwANMZ5e@O;tnw^v&FS zhqks?gMOP#O<@!Kei%(Ai7_!>O(Ww|BC_Ar7vo|SdG5N2(eq{``1c!z1E%PeB~&E= z9t`)HzIS)f5t7V2n{VeADu=!Ca(Ebn^cD0^t;^hmJsza`G?JJWk5bdWH+CZZn;Pf* zIe&w2D}e#s53P4)KJmFC)cfDcgI)#1lXwc?&!S>ZRfdrlI!`qOx}w6uqZS+ua~%nO zUQpB)x^*S<(YNW7PmF>$ALDEOI|H&Ub*oi;?ftQ~Y z6U=u2{m7c}r|m{?UMDI0-_;E?DdPj}Wz^vRb2%4Xed~0GZ-Suuu=+>G^v_P|9D(SE z-|DBJUl#)Lb2@7@kX)D9IMfdI4)uRJx|GJx+eipkn}qtqd(36HHMb#4{d%q_5S`!Z zhH~RYL;WOwnS~{gpVaU0U;uxfdo%XMv=_<;IWR8+*KkL^rQfh}sldB*jZV6__}3NH zZWHRx1c(2dCwez6o#GsX^yD~C2ux$A-M_nf_r;f>d49%4T-(R>X18l+su4aLy6@nr zycnKNQWP%Y$#m|Ug5t_pbidR$3{t!nCfBZPmX8I#{q~RBN4BH*0rMPh3JQ`nvwl2W zjqntavtMX{Z!^q;Y(EJ3@OPNk$#~o2lEv>(T?=?G@CzQ!UTNf!q%I4Upe7K!4Tm^)&37Zl7ReQXoQ_NlZjRKkZQw$e=Fb=93)Tf{82pjJp%Xyq)AaUx3y4#R#{yL{7-x2 zf9C27f9I?Gc%k>UgPBe2=;18fa-Y)FI5&N_YKe{E_AiU)wm$ZCGhPwOd;adoAHBXmjALBns%S=lvS>^e~ z)l1GF`sJH<3Cuo%|JpC)bm&JrP|3UEZSaZ6f9(hj<`?GLjbs!U&6n^k$hkN^HLmET z0>d5Y^R(0YU3ie(OO>|tnYyI1|*U^3{3tU>xAngA7``f#=LSQ|@*aai%;aUJMy z6kwG)8yahlSNN@aEd$Sk`Ww0Q*@=pEm+MQ9BY$F^zSV@yzHFP4uBo#LotN_0Eo?pg zT_t*s8&N)pF~aPbO6gfMvhU-j2clo)6nnE^ zUT9dh9iQ5<$CYrTWyR#=$V|28zqBq%|Ill|-}M$ZJjmr`p!;LGx1sS^pOi(op{X6j z2Lh}d=3{W>vi(kpoxpxVKhju`q1uTB!M2>ydYBjM+_u~Z@%sY>dJ3b4xLItDgkf$` zWquyK*`4c5oq?ZC)t!`KNNMF>MCU)OU>C{l+B?&xWru#= zgn0^gn>v|e7-c(q{E@c=uZxOPKX30>)z}i@jQYi?bm`yZ4ZM4W#ISle-x%labH(|) z?GLfijTM?w;5T?Uj@#$A;f24|0?QUQmjep^Ut<`axteaOi5dhE#u}?6#hwT+K>Jj(9dt(9-m*SolRJ{KK0<|TeRPkVysq5 zZf;$D_SGs+n19y}_I@l#H@jm`F#DuK0ZuqIRU07q9gE^)(Ce(79n5@SQ8eNY@q>Uv z)X%1U-|Z-hJa|=cfviGy0KhcuxK$i5oTpiyE z{sa1(>DG01b^N*R^rU$U2MQVsz0Gv{Krc{=7<6=DNBXuq1}l?Me_U&8g8_<{IpvPN z_R&dfewh7wIA4>d8I9$}Wh>vft*MR!JcLUnvK*OKS|;Z&MgZQhUl?}dyM2<}V&#`D z8Nm?04pKwOVL<_k346})<|O|9O#{#V@gEj}epB|^3#sNS58cQ>^E^Vg9dlHBz2;Bh z2!<`ueC~-mQqLeJ@%fwQ_D4vjM$+{9Z?qXiPRc zPGgD<^Uyo+=Z#y&$8S2e6pnO*KOE#-l)lP{jM(usMmJHyuVLdDL_TlDrRSAR(;Zj$ z-Y-t80ep^fmH5j9`8QMOHwv#>OPa0qmPYudc_{tnCn~*%%Str||J{XId2HCS$-#VB zdZ|wK`*zS%^etc&8XL(u)cz^)H))mu%}ZVkJLx@fZ{PuqyJf@Iz*^-U4cRp}UKW>961l2^N*7vJMDCR)GC=u)>F ziXX6>wxnObdTH}25Z@N+D=F_tHJM6F)pnb8*&zK-BA=wIAz11@B#`Jaip(ro$H=F- zmouvRQU7)|frXp1Hm{6#dodsR^J;IF+Faw_!Usf^17Lq)zL+MR5$}*BwQHhEvVI{j zpVlr(Ms4HSYWV(cYG`li)faBnv-K_W!Qbz8CgRjFkw>;F(#>kjo2_~*fN{f5W=Wlr3PJf`UndP{X#)LSwu z;rtKceaO}tFyE%Fr6FRV^1}A}#a*ChBo;5gRqh1+6oj|h>uam1{J7T{oZy4_V#q$| zH5-OFoP_Ti#5>`1n2bA3i5m&yXn#Ws#(BzeJ(>fAjKn6u6M$b^=I|?RUDt;96^OqS zR?uR_vwx)1Bcnf1zxRaok!8!*2!|j(cuuQE>*H$APJ;Ryr(xD>q266yTz&4i)bF8pRugerkUTrJBLIUW7>=$dGlU&@+q-%hmdSdm=X!sMLw%f*7TG{NP~&N= z$H(2i<}vFvqdIY8Ht;-(OS4UP+p&b3zx*m}P&^dP<$>PQaRJ-46#S!*;&IUW zy)lT#8oIUH1i}9K>Ni@Sva}MN<=uw$<6>lDE6?6?SE3)vZ-7r5sRZ4>AD+h6rH?{< zQI8SVlO}&rqEavDzAi% z@jNVIWRs2<&gQ-XybbgX92Pf2s$OubakLvtTZP(8fTg~ir0TKKJdqiNuO z`T;qY$@WzA9F4xLJ`$IM03LK%nGf~a(3kW^(usFcxfezXab8?G1pL`4d zLw;>I;+skMaL*NYCHjYU+ra%0MS%Zq=-riAC|V-v-}GV0^j)qCIkWoD%!E7tSN~K_ zI?mIpMSKC6cQ@|DJRTFb5-)`N)__Gsw!CFIE56O!6NvH?Q=Wdkn7q8idMF&~X~2g# z{VrzS4a;4^GdhXD7XZDQO4>pNm6Mn+&l=eeDnUA%nadC)-kGP}#Dec{`_9Di3yf4g zKXycXVh;589>6@}x@halv-bBN^b zMx}0_==bFV=Z;xEODahQJ#{MRwY`7q61>3T6fYI#4PX)TY1+lw(K2X#sy!UXESVRR zL!<6JL+@YXB~ANHzR$M)Ij#ixlaP`umuJoQf3wtobT`~T;DfzjZsC7SFim7mb~Hs> zJA)pF5cm!aT+a5Ji}K5}embWQi_eSoe?47*-XD)+$t=3O0RLm-o2+aS6yrI?p^W;n zSkVF-9b}&<&iY3$lfRG>J?!QuZ2tfGF5^|}J>W0w7S=dkWdLG%>$a+fHTZkba}OZN z%KjDAw)9Xm?6**q5@M&Dv-Vn<>Jop+{H-+o^}8GMmf4ZdN!vg?FOI5t$P`^OtTq`W zp!-M-^(Ha-%jpC1g%;BT>DKoRo$n)l7Q?1B70&lm^}~bA4IAuMH(L?M5kA7Xm_yEMK~TlX`}bDi2$!fdDfkOuR{Eh z9honmBuVP8`*A}XV*XlP`f>G~#rnnppf6xfv`wu1-pS~XRUZDc7{? zH<*9Al@I=VrrKcd=USAHnZ9P@VwL`^e&jl3_sns#gufrac=P_ZRHq^Zx=aXVJ{5}zL@55oPKoZ-K-D^zi~zK8d^8SaY! z>gn22v{YrOAJo(f^MjjD*sE+sdLdV5omfo}uL3@$bTn*1#|f?08cDo+(Nedu_t+nV zAM2_l@dHlhH<_s*@OOpyAr^k$cYUKH`QH~OG{2sN_(XsaSh0)-f!xMg?+kcfn14}3 zCk6d%pH0_T4F3FUwYRk8X|ltC;Ry2!5HAZbPfg|>9B%z8cks8+e<@x#+uz@BO?(bz_#Y{9k6$6&seJbfP7zQ>Kq*EyCIL@?>W8<**j{=JKoSzO;f+sW4bm7 z?=TA{m*ea{Ei1Avh5jw*?`dSJ>1SQ{swEK)Z@aNGb^M{dSaj*Bybpde=pg{#hES1I zX2tdpep>x3*T+f{FL)k*dpzruL@yQ#&aEf%9xmCKXjBB}_bVktNOp8rcs*HGorvlo z)Wh_a3I6;~oaQ7e#P5d^V!dVFl6?n55Z@NQ|8;Jmf!@3p$e&^VpkCy>KERtNjZg7- zu?3w+yb1BKJ(u*XD`ZG$aT@9+&<|$tM*V&ErOERv2jlA=eAoUoj7?qom&~Y=_ygxF z#^t#8Cza+#mq_^dDUo)QI0w$;aF&Ba2gpD@0S^RFy8D|Mv6a zWyFuGCe&z$Um*@kSG>L~sYjaX&pI$0b`o7JGysnfaH1EhO_KBP-;NY|nW<}s(4Kf~ zhyK~j(^ip$CYW!`xl3Rv^(nHi_js;K_~+IC^eh}yEKZ3MVLv)JsYIF;_uHlJPWC+L ze`xzsVei8##kB=_s3xKNDI%296fXarY`^_9+$Z2WIFF6i>-7O~W)w5N-x-%vnO4L3 zC2RKyqpm~#ol>>qXnWh?;yuzQVLbs~A!}+Gl9Op0l}DY>{i#Vg_|%akFL$^9G_0>- z7nKlvB9Hk;Ww}SqafFxQ2gb*2EqCP-Ru3P!yD!+;%^{hO_$To)-UUM`+h3wSDFbWK*h=;)&FP6X88Lf zRLA&4m(qJzy;gq{BL7jDqAsf=NHWNg)s*y`L%kX&HI!8%-`h~Deb}uowe?Goc=oxb79HlhL3S{*tnR4>4dzFE zOjxxV^et+kl0*cph^aRb4h(@jN)a%B&{jcNka3yPu~3o^LX*Y zH>P~|u?B~RI5kh6Nb-FxhSWK6ROUd_s>GWM?~pc@EZhtApx;tgb5$=a%HQqUr22MP zO!@88bbr_)=hYb6B2GCeAzs* z-NrX>$AXIlo&1}>6WI?pc^`p#n0QTe^loIYt1Aa>$i;usz0Zh>z~2o}(<5>}ey6Xd z_Iv>R8|rskiyUlQ|=0j$oq^|eh+Y-I;d;%Fi8m$G2CcjsaI7; z6cpWidN5FGq&^7!ee-mE1=g#mrQFt{a}X~p(l9DPv($n54Ip_*{6s!xnHQ6_ZrU-)Uawq8suCh|;_Mw4o$@FS-_p15Fkd)Y5k53e4; zr}Yx`Z!MiKzNr)r>jnMaVmo$WZo%lanx0Ll|IAncC+wvgwx?HjqI?DT)A7o7nyKZ_ zi(Yngq>xj8u3Q821SslUzHvebVe&uyq!$XEk2>!l|3nRTB$rC$_rApeGWoe{X0yel}NMm6LYTe|QNZRG~j4gnC~Q%|M3<^?*x+)-X4;pVgs+$0|JoI$5qexPbq0@h$|Cb>pMe>8)=r zN%%;S@5+>wiXIs=>u&;o1o}PY3NOv9j(N09#De?L5tbfTG&0hd7V4#S7V(qCp2VR% z{Z7@Vb@f+hvL4$#Y=w7(lfS8UoG)(3A6DM&ZFgvSI^}nJsT!>RG+v8n&u>*^3O~zf z(+;}Ayd8cS^rKbB_m>x<^{y-re9pjWnf#F5`BB0b*KA;3WB=HEzVwz5ov$7HimaGL zpHr-_hg?VZ72CHfh%6>#RRU}b_|>F2<5g2vTjH&NG-WkOeJuD`0FN1bBUDEBgd`uN z#3|GT3!bnXZdF1(NLZ7yOT0q*e(Kw%OqK-SVl%WiXzeEY>zRtsdW(`7G+at%{mmOL zQE)#SxtMuielC$0dd6?{Xq*}7-R2E8Cpi?atr_=$c;-DmmDoytY;o250d-5gMliw6 z&M{dG`qV{Iv%b$fl+sZ?ARTct!X-leH}bC>ot&I-##a@on2;b;-!l)1#J$g58@9!w z>^6#5YrN=lM8a~z2>GQ@--P=a;=`@8mTn`KYYCbfpnpiEC~2Ny>GIysvi`^kT3%A94+l{`y?ZIcUvXr1IoxWjXwNCl*3% z=5A@*_O4)X+2M_#PiZfFJUKE~^L?d~6xuJmlX1>GUg!PF4R3JS@1YxOWWx(<5~;2^ zd7lE{%UyV=|DDR~j|`A89lopLQRmfiE*8aS;mjBR=((-Bx;(N4S)>nXh`vX2I zXOcWMA`b(X?x6V4lw|vlg_Kk6^S_KWB=Irmg=R*s-S;JhYoA^873%BMI2?)8NKR%* z^v7Tx)! z->u%;J=m8r8qEg#C7bj7gqC&Hk2)2w7oD7l7rOT3)lrja`k9h?_LE0tjxnVlaaqT8 ztdy_Rdj2^;niKtcx?_$>k>G{Se_wQ9-aQm47@ZG%x{=UmY309G3R&!~eNyOtib4W$ zan{y`)6G?Ww0XA+v>!n|PdxQh`XgpEuGFwNrNY)KR`O zNcDKP#h#n{$khFfy(At!UBKu&nssaDa7_u|mxK7jSHl<5NJ%>N#)GJyXzI+mYBBm& z?*~`U2i*ec+rl=rMDhs*8@KPO{?@#%emk$3Hd(ge$IFv8?#{aJczxj_ZIN2 z5snH@H;AmN%KqY=1?vU<8II=WO2_p52y(rMKge;JAKiEE{TZF0W(wkKP|^ZiABV3Z zAN(enM-O^hN-YjgJ@LW^d|3bQA5gq9ruAVyjOSBF--FRzo3ju2EvUJ&rYZY0_6L#k*rj?fU_6(wUmuh^F z#H35NB%TZFhJG{wJ|sT6%4}h7>_%6?&8@DD?sm!7CcpizdHGTA2-u?m&P$n_;W?fk z*K2(Se6V5~^yh2|x}EHh^VK01-qs!H`!wwA)XD$MA4mBnHKpi4Z$M}Kqh*wzV2?XF zE-bwz`gJi+PMBzeTbhk6oIHkb^ zCKqDA^Bjj@e?dPewSh~wRm|1#Gr`dLHjg;SE8YIpZr8fDy~y8TUkzZMM6*KkA6dib zd6QX}lic1OsMUrFZAU7RiatzoDJ{p|nKBfMJB_Y?H(z@Kt74qEb`_Uu|c z@!SyE^XdX(jxN33lx0sl-t;H@oc26`T>hd!zhGPk)_VXu|DEtSDf8LpGv88Cegt~V z#XD=GUt1hyQ&9fKNfQ@d&r`hhByqRgLRxekO`oO+{Lsr}n~RqapBQg)tVfS@!mwyH zs~Ooh4&9X}lc%9S+wG@r6A1pvN28V9{vxy1fPNeBjOH8`;4y$_XzTJM{*{svqFdOm zdZVm9tsLT)AC=gViyB-q^Rl;tGw{W#5MK;;-o_~(YbccD$2!u{$Cx{xpZzO78{%P@ zZxe8+SWkP#fFuR>6Xvh$L=`_A;icU>p5`yH-&!)++b?G<>GM7kEjce2ZAsQ~{miEd zEIMtChi#{WW1AT2&0ik{pONr$BHl4sMz1r&6*n26_=6JJUjIVLLe8Beg#9)7&IvXU z^VJTonLSnMnBCEejTGCTgn@|62d=n$gwLAZkjXAICU2Hj3RGXX=uT;YA)SQyWn4>N z7m58V$F&^A>~1xyo7fG{XyAb%NMZCI`HV{8ke7=0z6(nF2?#Rup zR1R)D(X#v;;@j$lkAFb+k`fwWAaEEi1YM^>;CsKPzB<_2th+ed?~9cH=_OD@G|3g$ zS3bNg(OV=5L0?VbRDiwm=Wv!W@J9rAq7KaWksa^WudoLH{%Eoo|9XuPlpy=)x|!0tWrRD8|Lwy9)mL|a|*^!q4hO?UBo1jiEcPl?IyWqnPQ`H&@aFlq&hh^mgnB1HL9+gxV!Dnv%6d5W5wc2PrIhfRKWh! zQ_mMcg+oz|;5Q^dcqYX>IKZ3hdH&#o5<8TyiTP0f&Mi1ZIVu;6^bWBVS9a3E<--5` z_wiQkXD&}wi&WairY_gc9Cn2K3g*vO2WLt8X}o@Ej~Mm&R!8zP(Q29j<3gTr_^u4RaNWYmwXBDO3R=vNyy>%U`XF$DV zjITWJ*4W=^ahnjI$4P0xjm54EUbqJ0H_)4odS3FRtcjIqtF9r@dm!U@K~^8ntWC$Ts=}y~lzR!d6@EZWo{K(O{@A+#-#+sHE&{>(sd; z@o#cCk}jXw*fqh!B$g$}OU>}}uNLFch`^zh;ED|hs&22eE$ zqvjEY0wErRe)O$dc!T?&`2F@ii~MKJ)_0S9<=v}GuEdwzT^L}VzWG}WnjdtNYDh!* zBX-`WnZQ%rwYkE#8_gGl`I&aVm|1OvZy(|Q>cV+y=Z!U!O5)ChR&PT3XFiXJWWzhU z_pT_$qj(YGKi=dD5Fd!rz76?bC+2KTs@vTkQ%k4EOXBJM>MGY6mz~bG%9XGDE0wQf zcsOZNJY#-UI_)_8{RWOZj&#WNSa-WP&SA6#)em@KOQe)!W@=Bvejl1N{h+>q$rHY} z&d7!Qskok_pkY1v%lk%<89fmA07V0sz?vatb6E*`+jLku{9!5uzxrJ&R!> zIREtl_-C;16-0UteVylK&NuABMpuH9-Rs9_{TJ{Kn<0OSRa{jlZF0^iUq%^2@s=qr z?&`DI<+#rTk`uzSsh&gXfc01n~ct&-Fb}lYgHf zv++hEZNBHupTDlKrf>&dz+E(P^=vT`Pb$3^hxN)u@_QE8uqUIRWY6RXx<*=n<_1h0<51_ ze#d$Y@eMgn21K4tvYK_`gLcSI2C)w^INZ*qX~U|w8iD`ejPN_Dx1(q3WKYS$eH{UQ zaA}#b$#cbXr5-qQpkEkn;hk9+_ z2uaa7lJ-JWW^6~Oh#@V;9Az6qQYS>E4njyHmF!8#Qdx?aF=L4lVn~ZI$vW1gI%P?+ z&RC`~zx$bU-p}vzdH?Yqjqg0)=eeKjzV7S3ZflDyxQ})>F)%e2mJ*Mn9+=BS3-iOX zn#yJNu39N;qT*0K7<$;Eo=Uuj9g!v<%~wDpf4B$_%{-=Bs0WQ~D= zdLiKVIiqxI6BE&7W3SHk704gNsM5!olGXT9!u6D7f~@hvzx7sGmDs}ZCB!dQSII~h z`mR-rV#@Zyeql0_AYaqkURiSfPOUcRxs9qcu-JcG^}i(5_Gkm&L5hIoi%!io%6Vo3 zzdtGnn4V%$E_nwE6h%-!(U%^`(D4--LHS}O2PK~f1r9O^i$~g;Z7Mszv$6~j<)~mr?Jrk*PIj4`@!Xr ztytKGo?GKRwOKt-k9m9V?i+%wUPk(TQ=#9UXZ@5d#r~jlo-YRBXTCf1=e@A_>gj3+ z`d8@RdrKpe!<;w#bX=`(h3=a)YW=9?DB)7l*9$0~@FR(T>#$zayfR)|3hUi@_O@on zo=!+~+$@#-+C}`z^$uN{xDjYLDF*uF1}^XEIrD;*-wKMOq~ZKRzvE3~^7n~3y1u)Q z0^|!5;Fk~c;~nmB&oTO2@526yXll?XH>U3yyQe3t*Lq#gnr3e-Rs9hlS$F2*dLHO= zB&G|St~RNVAwL@-xrehm+p`Kdo4thnMuN0*a>Sz^g#^p4<7EpOEy&<&F>spfC;@N@cnM< z>)C0BV&_6KECc4}$^>!l+qQ4&!5MXPp+?Vt&(x|2t^fEa8FELmP>m>hkj4omR3Vv88 zrd(sn%|c#J+M)itvE%y{j%K8ux)1vo{2C%?+nbxf44+))f%tA*8Wj^hvYnYmKH>#@ z`9#h9_0NK{TWh~PcmeV5*gQXg_IQlf3Ta8ZmlfPkA-VPql=2@Me@+u1QQJCN2W4fL4V_(mo`&Co*-1ob5*n^&uU%fhf<^{y_9#RUX z$)42vsM!WvBIJW!N4%ny$q^5&^Z09C-2ISqTleNA9>1pRxWNf4;ERJWhZ+bSDNn?i za)U_k!ON4y?5Wr~9_ zxAro3@M|Y8!~Se3GH(l(&wI6u`a-y#dQG0Jj(Sb^?f5QO&pE^HCIV63Tp`S?7WhIm zU;4U8{z!S5)`GY#djFvj;f1o~u)}WNU0UeA=aR{oY5~6t|1O%FK8ewW$o<_axBQe$fA^hx6-@p8ON)#hKejJETuA^tU8> z!uM+w1i*ZyXNOfn^6-OJ=OKTgsP5b|u+_`EsB3jX^8ftq?X^u;Jp9=XWkND!1XxdGInwlrWHU zsVvwMl*Z4Tc_!>fD2Drg;Y!VZ+cTMpqBmzRqJ9n>Q#EG?)k3?ywkCbOZfLJpE9CpY zFVpE%IrM(3J&jtA_sMs9k8^3je<1yhE%C%w{NN@DJkq-x23ywX>h>50byU4FU+z=? zPr{!4XkJDtDG=~p1Kgk1RUbZPZA-jsu7cv%Lh?zk$IlGz`xBmvN*#2epR(PFMg2so z1X#Mj}1RAtm@QZ4Q({{1MQWenwz zb-Xr3wY|S~>G(l+{gYM|*rADEwbft7As!*Wo%gmZ-Gjd!O8oT`-M6IYy{p9K-$=Jg zGp67?aQJB)TZ-8FWm=0WYg+YT{fI|9DsI_5oNOf@MR;hzLzZmsddy0~;S=hAoKF&NK~?l`l~K66%#pRVUkJ)vB@pKO%mX+n$u-(Xsm1`qJl(@r}9O8_ItQg02w= z{%#4n#Q*Y-B<0g9G#+V#KlMTopGp>+{aXiyzQcT6Mosci5o2j+yNm*Q#h}v1oxK!*JvpvX}4=FxaQqQ_YtW_gPooKC3E5Q!*U^CVxPAx#%@{W6MTw_ z)Pje)Qqo-M;@rHTSNF2|WD*!sE(ulCXM;U3aZ(ur)Hy;l8v=e@Rw)$K_9jjEq-2d}! zz*peEeJfntXnJ!X9;bT};W?v9s?B55`>(D$Ut~Xj9)0wQX5*jGKNB+3W_wk^YlX)@ zZ(aD~6Qyf*Ui~DF>SICv7WI_!4^LyIQx#FXF=Tq%60L9Tz$ee*h4^C!z17O^mVR70 z1Nb|rS60cy{psiOw7EY1GP=(NQI>>Uh4nGTZ~O8f{xmTRLT%MGzNJi>Zfk@1+rl`E z+0E%5sqUMtsS@rthjwdf)VAL0pOGpF<;~oG=l4D;KFz9nAf-#xcd!baMg7vj29tlI zdJ8`_m)tb@%D5-|A;hAsowVzzLnwap^k|ru zqz(Jn-P|nnJ}T30opJvsT((kkrzPaKjW92Qv9vT^-|JSB40vU@GFetcEWTZe+I-Q^ z^C0Na*$T)nw`S;gnI_T$!2D+uETw;6Z=(9?Mj8LAuk97!2gR$EVNvEA5Biunt_i%1 z{ByqevDHz2$Rp+u*mm3Qhpnjk6oxVQgS>%WudnwY-&1h8hc`k4Jl&~;0 zU#WH~tyajV9BRT^65H%Ts1pKoUkPsSpfu&1%6Yr=bFbKWzi?=KBjj(&rYbiI?^jg| z7OVO9$`b0BU@`#tk2f@08Jx6Wnr*4cGWaT(5bP{f!{R$`6cs6;`W3^;=e*hD4Qh97 zTP&>*-<1OXTm?JF2iJ-T>$%nGULizm!cN;N9jq0s7wD-ejW&Z>7oy}B;r%`P$_U&S zE+R%x>P$R_>VHOLr2<<$2QkI6^Man8-bB}q&dZ1&5i{xHKzte`?Gp_zWW18TAlG>Y z>W34gC`&aq|L&{4r|qb}0Qk1G4|LZ(;?one-og9hRtM0O{mSK9ybq~(qMsM=-rZ%G zoS17p_phS;&2Tu-=6KvR+-KvNMf5&-(4THQXkYtz&3>l-roOao`$}QHsM7CW>es0B z^rX{sX!+!iyoFHJHol$}oR?7S_%P-BJ(YvblME|6h=+I$awpwJ%;ZyxH{u&`&escF z%kAH)UA9@>$hvYQLv!zCjzAFF)cpeLi@;Cnb8R(LR}@A54)+t_CDIO?nU1krR}NLo zJ0pIauWwHzo?s=YY^p{2s|f+<&D%*W(QQr)I(pvf+@EY2<<1plrz&h(t>F2irkt@i zemgDw^f&Be0q70xP+E`g*KELQ1OVPFg7ZkWVRL?`wxwQFfp3B?U zecT-g6-t2>KX>4%D_bkFCtbIG0&DicXfZTOOYb*0ka`a zq30j3*cRwfSo{aNZyAAI(>7`i>%9a+rTIHth|V@$z;7*7lUs`~!1)3{VQJ70pRW0- zQbULMK0@-ZUF40h8e=2o-2m^*@o#gd^_G25eqx&b73v)mmFK?MJ`*{*Tz~HkKcv^- zT zYh?`dH~qR$JbMB3SS>gYnw_)P25gDKSTw}5Ia~p?)v3GfcF^u`i3oo&@}YVVr%-ya z<{u3de-}~$ny~nBrG@N`iYWdplxvz5mF47G?sVIZ?k7f1T=>b!jfXI^Po_*p2^)BT z4_A_vga00!9u(r&GJRbe4$PAj(eXm{qfoqtzOK&kB5#XC6}V5Cd^mp%+m+OMlS03@ z7DIkmU`Y|1JSMMZJ*@-xH_RWQMpFb`ins0=e}nT4ewIzl86A;CZ?_pO&?gTXWl&>y zOAW#2jsN*5tS8>4tz6wymww?#`=!0|E~r1w4xPV$`sv~jj~j94+$k$#ObFB;9|gvn zc6lfjHpp@DKD^`XOB0T)lviUH5?A_9DE)J>mwMc?EL@oEJv48z;L ziTkOAgHhU$sN_f%Fy?k|etx!PQ;$5+)Uhqa1^n@A`gFVWJy+eM=hF(vNWa1449ZHW zg}iz2+Z*SpiN)ZxB7!}__;)MsX8Z%mI-eo?9a;Oo2nGe4sw8$BIx{56;$bl{at8ap z!T%>9#bEq6aNqSY2vqfoo8lVpq^VQjx3Gxt_(?^Zw=kvn9pDG(m(XNgCrc}6J_Fav zk#O+K`TnID@{giU7lj_X&_gHKueXE#^Z8Du3-njEaQ3|>=alWYkQ3Raa}W5-30|I9 z7EO8Ra(_p<6vVTM5Q3=4r_c9ysEQsy`)^{wLbRSJVK3=9>s=wtuOnzn*F)-YPkvZ8%}XKlP)ql+>|-Mk2!!o+MZpOB3;aUgiD9>0`p=&mKTQI zj4hP59EbSc%fPqwR5{U8Zh7znz(R#IW_H`Uvpl^Q0X+2JHvODOG>1K>1)3>A8-lVzvas^y@2Yq(_AQ+^j!C zw`}S-rfNI`Nd;TM|p%la66T;8s(-*mi-$!k(*1(w@jw~1N!V2YqU28 zub21U?_kpnc%5Ifax4(?cfeQBpZ6@RPp8lLGwf%WkE^Dq{rGyz%TqYKFUmhisu!&Z zgx7NSD!e*CKgtxOd#lE&_ZV?9`s`6XgBO?=E8CG&h957+qIkxC&iMlym!pUspY;~Y z-cY(}eIoda>|Sy2RFg5}pE}))P5R~JLfI1+E?gG&v)_MteXaEx?TgOLrGNDY2gY1( z#_oJ4#(;X2E!-DzWMaoh9p(*<7l7a4{ELxoX4144pB#hx^vKY@O?r5*^UL*P*H-|) z2z=HK4D7xrdS9Uc*57PYaQ}d{#0jRt-4$^^(EZO(^Y&SZX^7Te$f4Ke{L5!AzjsQI zaOuoG`1hj>4|0=L2Q#H))A>0R-$M%y`8Yp5nC8ED+;8tlf|=K9{8i-dbTG=mife7OnD0UE6zTBiz)U@@)Pm)0Kf+jZyX_iZ6Jt=eE22ozX~!?3+^U5GA=$W z!vo*lLQ0oW>~=W4B3UBnF8Y1LKuxvte&+4I8vDkDe?Llzd1Cf}*20JV-U#o90^V15 z$SkuQ{{;Lj_-)^|2n`ijQ`7z_HxTmm5n`ftp(m~f9pA>a0{)kj?%#nib&!u8^{&u{ z{F?72L-yq##ZLCbo8V0Av+eu_^Dzb0 zXB_o40nadNoV+O%Oi#-Xx5fapo(s1%qhe2H9>x#Wt%i8RL0^YV>{$E#gW)bY|V}?SKCot*22S9Yax@Ao#Y2 zyTShdPO@*J6zv%}d~Yf4Lc-<$^=C-m{xhv61NuphICgvmKX^ek%eey1=QAcQA}8;` zuMbl9?lOK%mc9o3L~xcDmz4eRJP7tP_%rN~l+hb+mA{+V5+~Gi6x}%;Pbn*VBeB*I z@aP=xRui-3q*LOBWt*!EY{z}_g}LmJ4md}00BvbFse;HOJbb6ABE zMHbfoa`s&m{{=^)m4C^^?(ljydja?uFa+rae{uo#Zc_5MRyN#sDP85OR+-%4oT|>W zvI6|zSPu8eh@o#k-Mkp-$t0x-ht7y9-&J*93Tw%buR8t>@yn!mW!9c6Fn{e#QSW!~ z%QC8O?-t2=@{iz{;LP2lc)}jrT-rSBhdxplt<#SFK;zoEKUPYC{w_;H?L6`Wh*RvS zm{+-N{n^Ldk24pvf0;;_YwI8I_rVB-i0yo(27U`8D(*B-JCBuUzU}9@yeDY?8QSNG zVhcYWJ@?QG1Nm%=pdvm;df{VaoLorkq&Mi17!4ijVnf}lx9srRHIjHJEf)Om1%hkA zY?>y-vr$r&MheaHh*1ss?H&{_LsAYiOUTAvd2gOyfcqKzZ{EkJyg0C!AZUmELm7p6 ztz{?5FA^5Z2P9xWaBHG9$Ym_c=*GS!f52ZYg4eWG6UuA-(}k~2B0e_szJj!Lyh)Dt zh0wsr`NX!z)O45!E4cQ))8Wf5H-A$HzxDozZ#>sgrfGEu<=0thbUs3oq2G+Xsfc~v44&s07v#u(xiOucq5_j^sQ)pvmr9;hdsr-S zVsZt-vqtGYwi?(kPuBQ8hVKjg@waG&u}i;K7PkDz_f*Ml=)Sw*Du;jd*ZjxHz399% z=(IvBuMI0onimOmXJmR&f5-Bcu)^H@MyIg18S%!wL&AI`n;9gH@Jn?tv20bGs7d^A z6&3tzCIk{YV1E6UJH0uU{pkHvCZC*Q#qEdrRx9Gr`d031t11*rSbJ%R>x2BO1rcH< zb@fpS2Q!Wj3hNhdj0@d z#1d5ct~j8dNx^OYE+!x8BBu6(|LIRiSR2Jfm?%*Qc!nvkV2?H27>FpBs~(T+iGGns zjiUVIO$Dp@!+hR@{+5n*(7z4+9?M5hK>UIJZM=`yZkdaQB@MJFI3LqS`~DH)3txkz z%kcit`bUS4$!L{0mjEB~tdSAfD|sU_NrvO!Pepjh@a%gvrGk@MafxN{{NRtC8y{aw zk3J|N_uHccTQ>J!J*`6Oh%>Gdtv7BbW(H4mq_QbBAAw)u@J2N{KYr|#^)gS`1^px~ zjIMYqFV+RM3_1-NDDe}eq(?FI`%^Lud$pByRs*f@tavhpYNbg?EM{PMK( zrhBw`5Qc#N@~RvqLVP)&Cb%DN*=&oix!~=-OCE1Xyt}-UGqzN-apPuLR9^@>`@Ip< z;rhI8#nw6@zOK+Abl~ywo|{5LtRQ|@r->Gn((Ugq>|$x4{Tp)2ro8avoxQOwBVK4f zfj^=MW$b~X_nMlARuI2x4%u5a5SnAtuz_TBKKX|%EGuR$blhCJpdJDGZhUTVF!|)G z>+&Z6??Sy>&xSeGu{tXb8hjD{H;VZAB|F=iS?M;|h4v$#34Q?5eYewhciZI8CzcKs zdr?sSUC=4eNBg^`($Z&++W4q#(|x4Z8DzYL``>TFof7%}IC!6PHE+2#g=C9WldW3Q z!hAIFdwy1qd8x#=lqutf&#z}z7VulB7$T+BKr4FB$*JX>x zpS={gMiHO7*~j!j$Kf*m!nLKglgmW*$`kiGbb#Im_TN*lYFVUzV0d6)zD-+M+?laO zRNoKP&$$YB@Yb(&8!fF^pocxeotYYAHkJ%=6T@0#I*inWd@_kF+2tVY=PhdR!ip)- zf7~^M{0sDcQ9kFJiBI=DVD0~0jOy+52x%;Hf>B;y1@|Rkq&j-T)~%Ao54%J9Q9T#> zrKy-K{!^pHuL?qZ8e)*W>!aQE%r@uLQ;471m)Er?oNTX+&-()L6V9hs`D9Pe&Auh| zx89J?fPPR}L$Ag>=k6I-oGW5Pl2FJ4yoKXtC*bsZS`MUGQ`) zCzDGjCG4{#(0-}}|3S)b+00KXQxuQ$PCR$>8M^U(>s_X?UUiL1%)jmp0B0Z+ZyeAm7bMfoi0tfnr}f%T{`I<`?8 z_;r}~VB4?~cRxk0LJ8%U)m|Enlg_UnB`C8mqIyqdu2`6uq}+1Lm{~XDERO)C+bN8R zIpa^uM6$FzS7p%iG)JS6UP8@wFuqaVANkSRK|gf$Q1BkO9{}G6?G%tzv45eSD^jMfBN0Gysaez<` z=|HlPOSYRwGNsonjo^6jYj+Y0Z|I>|9_6Y7Xs252VesDgHof04PHW?Mxmq}P( z#YO)@V$uA-m8$KU`P1XPIoz47+9ec!NcK6Dior-7UkalJ^1%^YMqGRZ_(_kP@Wh~e z3>T#leX8H-VNg0|7VW?4)PZsx@}iCw*+dfFcVmsyw<3mMIqvhOMpfiDLkfiZx@+gt zWJj6l{8jhrpA<16ek@#L`jYm5pBjGfcPF8qd`Td765%_e^v^6p#}|FMF&5*wNV481 z@E_$sys7uw?VP1PAaI>K@6pU8*q)a#o!BesU*$Ox~q=)q{%sKIPb(1RMZ*Zs{ z&QHsr7xF3)e#SZEsO`quW~I@=F{v5w*sa49wo!n#T)&>LupV1Zi|(xb*w}Zr_`(&H z+u%>SHVNUc@ciz7Q2rpuv&g4lMR(!dhzz(ddW>%UwY0F%8{y3gxL__G_cp(1?*-)l zC=&L`73O0bJ0x(EI5UEp5NE1-*qsb*gM9^p0l-YyXNBj5^#ms>**tWa(GSOg#OLb+ z4O0`}o`-&>tD3rV4XG;bX3kc6m%*R1CfHRA;VDkgQwx^unveQ;IZ4oCusHiHO00am zmeb3>UAB;#jAn0-zJD3b+w^c5d?d`b?}uB2CZ%Tb$rUt&2Sdm~dK$#?za;zj4z5@6 zZ$8t3>Rntd{_*Y8&C*rkhgTtZ`reSx8h~KI? z$DeqNJ-ld3-BShUWrE?L9G%lG^ECE18BXM?d$0409AVxe=W3%xH_6k@d%t5R_-5cmO@r=Dw0bU4FH>7a|i`5PteAY(d+z4|_@d2Cdl;{{+=mVNwFGqOqE2h}44 zU9`fok;kk14!V%7P`|np+g2}UM+LrKoq_hNk<+bJA3GQGIMG|PApiU>ICPX#K!$nW zURxGT&dLb;O(eoZh&E00Ex=C?GG3=($RAD~i;tA_v^oaoJD9pkR^H0BdVA$vGGa02|Us`Jz}^(UV-qxtS$F0u-xBcHtd5@I!b|J8HFrY4`S--+^9p8uQc z6!MVZ)}N({LVaen#;6Nb+?;5z))vlR3ohB-zg)S9kzC&D)}sP`TI`$m5FbZyEm8-( z4D$sv2+iBs1rMxO=ArjN+Veh#{_zsEDtqO$Hteq;i)OM;-%acbYr|;xya|E5eOnvT z^ktkz;6-%b?Ym7rXGT7?Gt>+>J6Q_P%NW!Z3rk?#AE0Xp&nvmDr>F0vwq=K>06uTX zkRTx@{T;XN{`&iOh4?ZU2F_Ct=K1i!4-4V}sX(K<0sr)fb1ja0$8$82)6*D|H&K~O5$^AZh*CTD?{RJGA0mFPmyaQ;a25AADj0gF~e4*z-y@o9#Jj>)2;vR?z;@rg(;B}maMv*Yz{Tligr zX$5*Aqg)y}`uo}H5;q>?`{19eW=VXp>yk;tiH9$Mzv1xSG?^C^3{GeEM|#5d1OKU? z3}eYwsW@p~C#q*vr{vfeD_cJK6ouob)_Iz3i3ylxL(G~M2*B@nZ-k3^JVb+%4bQym zB-e`+)G7Gje~j~ayo(&8wFnO~D#)XwVIs=rU)!sD#QkgkY7LAYpoR--K3 z@{p)?u_hxIm`L9$11GJnW#IwvDNNYUop%1cvdqZnScJbx!3ARBt{W^9TusDKeJI2$ zJCxYbceP^BZne~^3>sMt^>fT!QzLS&N#$R$%#6D~3+uTA_9G{R{6aMQ93FdQt6N)Q z6w+Jq9c9Ws6xu`-XUtkazBTxm6bQ0>|b;^km6)MHm1cQifR?^tAv#!8ph#B2G?O>^ahF$*+^O0{xrdR(pr7t?It6C8`MGS1N0_Z8&^1G3%@2qqXIc}mn}Hxic4Lw*SPOA7fa$zeriNqQn4eZFDTl-IJAb=;$0 zvn+*t^bvbDi+T4;A1#!J@(aV^2r_xPG7(#$_Frz&E3$#l@hnqcd&os z#~J2T{^Kk!pFfgbl^-{Z1^#LYKkE56#M@q6iYD2PqqMqRNuy7=o)n4Z(*2iK)_$tw zq>3cJNU<7?0l&+uCiVuhfHwuWvjNr`-DW13vB(8;UP@{1B{k=dFokQte{%%NCqjaZ z^mWy1$GvTH#Gzh3RBfSwk-E~ivZgWSZo=K>|MZoAK2cc_K5G>Z_g}CjCCm4bm|M+g zyusJAzC%Oqzkc$H1%tJpm%;P(gu0WL7Rg`K(ogy=H&LR_A^)+b)B!!xZoOq$&rmId z<<-QrtPruMT|xV=k{DnXL;a-2$!J;uelda&2U;{3vfuRF!ok;ln!SzAi}u2)g){HY z;2_`TghVm>YUv-SiC+&2^D&wSlJNDP%PoAXUiTMv1H!j=TV@`-+vf%KBJdjwY_i3w;Y(#I_#rTF zeWIpER8nN=PM7DM0MnjTF^&&u!uh))H)h%T==ae|-QxDgox<9dfW)mxTHQB$a)P!=B%` zqd)ou?khC^kbURhex8c7pE+_gt3BdD%F)ImhDK}RCyiz_HS6un%~)m>fIzd&CVsVRrYS8oBN2oyRnP! z-ZC^#$FR#m4b>Mz(l~?a7Rk5*xgCd4{>|4X7t+FS=9xd_V&MG_^DNXncYPAawTfX| zQ9fC-BaeVBee~p%Hy_>~%@% z_cB}D>ZK&0UkmX32w@TKSF4^AB!o`{^d01@wmtDv>nnkuaNq{fceL2~9xXm-)So$` z3;t}K{z(Y0PtTR{F)QLAD!gxfH z7$rJf-;sF!c4$w}HM0zC;4Erm|1#hzKlvxr?~86I;-@~m;LrTC9j%vP)PNmTI(S+# zMO)N!^Bo$w_33%Q54D>}KKlW`a1776l#`=>POo^_YAVDFhb*u}KMQ4X>FMgpB-GDB z2wg39Fl8ZNH~{6pMp#jiN11HeNd4Br`Nl+M26clGvb)sD>?G0{Ckdtihq>bD8GEk?HUKlW0>k%x8bk4AwgbGt675>4DwGR`72|I10JRZPu?choyKe!8x#m6-z@VIc~F-r z)b9m(J&V>kB$mH~{QC$TUSs)*K0B+y#D2P(P!C{dah_c=+6t#tjoIbLn(L@3w|aH^XTDVZP6CrYQf$i#H;=m z?9QvUtJ%NTG^2P%s=(x9W;V${ykF|s=}&a^J?;Xm*ZNa6G$#yQ3ZM^)!1%wBUB6U3du0PUQ zk(dfUBmZs7fs;Bt)BtaeqRH9j^{D?_nyC10@|~a%`qd^#$C)Q}$y-Z(zFG>h^u7ot z1pP+7)-d7eB&F(#p1Y+>yTWA@Um}!!oFTr>8PTDhVk{oi$6$7B{A<`{WZ31}#LSAq zxu5mR5PmasxAC)NKVA30bknQ4^s?J1-)HTFmiE8NzMv=D?`YeN^7Wcaj@7z=mwItk zR?T|lJi>Yr*+Wy&d{{IN2E-ujt58)4s6 z_#;*2w3)qJz)ki)f)`a~H0dy@*xhvezkEF{s~rdH4f+ZG*u;(EsE3i8IMDCK6%K1C*F% zY)`bh`(E&WIiY}f{u6`#G9iACRthDt-Bqo#WEO?^ zU%}#YJ(cNMT5DJhpU({q4A>oW^U1@`u*hA)`=H$3nz{Y$$o0S>Lr?Qt2h!T19})N) zxBQtGsb#*nljnE-VlTkNndM8{V0}h$XHWWebTqs-y{F?1_orM>_uD9Pfmm~HXYNWA zZz~O~ef&D!ybl;Zc=XDAbZwsczw?&V2!sBq`)?fTy}Pb_9Q-bW4XNr0@&EI8gnmG- zohQ8CMk72P`i*|7=09BGqI`j&+5~jJqjNu(RIz?Or-i}`-FP$r(IU2tmO+)f-VQZ z>%+WrrdC#yhl+kUS9xFgl=!SOp)ck$e=aEB@5dGRedK?okCA>K%_+qR^Ocapc`7#w zPVS?r&LjNQ2lf5tc=`|Q)Q28fp&rAlM0$k%>EOAY-N%IdKw5pMYxx1ghlHn49|QgO zeaTWg&e|>GzsIqKdY0V7E&B1Y2cn+f9yO|X?{{ijs{-f0s^jA-lrJ#_hs7G3e9iQ` z3cYz@>#-o4i>1n~^2AAnybq5JBeVj@XHenR}5E;>!p8$4_?JZWjNy?GSriRxOK zny!+s2EzXBAsvawP$*G(ao!<3&&fpNO|rv&0O~`Rw>a#+vJmZup_rcX)pE=h& zneqCME{tG*0{^%ZUoZyyw;LB^dU~p-uef<9X$gHEgaw;0DmdPmX`{XNM8js=JMp95c4 zP|?%k6*JDCuO@tMm1zZjTyR`1_M?qhs@v|0Oq74|%`MAy>5(rP{gR5Zp7TVNITvHW zaIN5ch4fm`AN(052rMDn*|KwU3r10X#K3A;9-@4Xdgy4`1p5)@+i5NtFKxkbI%AvR zyntVJwfhb~jq;Sl8#fUD5<~{U(v|VGC3nRYQ9YnK8Ka7GWglL#MY~G4f6nFRFB0pe z{m;*xGx)MR!1LSbUq5GVJQ5rOzB?B9Ja>ERvB$T%++Vx`JOhG!M^TeQ_5a*{RI)Fi zXM|juPaUP8e3F2 zp#8n?tOdA#xtgdtlmdPR{>piL?E8F*aqEGMD|u-_*WiEEgA+`%x3{&SKZtZ^XxKSm zszN!d>&@%y+UP!kdPtdF+Pdk_v>yl$R3>*dJM52?KS+FEl`8LI=Ggile?3T#Erxk- zjI-a(DUBOlR`3>>={Eu<>b7KGYS+6#}ZUMVv zSmENCb-wW5{YrNpOB4rwqYC(W;d~!!r}jmJpZO!i8DE){PPIpqYUq6g-9B2+r0gj0 zStCA)@=bn_s3M>KGosY+h~yP7yL8H z$=k~3pGTXf6*-3fg!&ZOren2`U$e+n?)Y#r%QF@9y1>Wdyoi*-c-g2z@sK>_Z7ax| zM$!a#W}a>1RgiF?H`wc7{TB2@{ObP60a+<|Z%gM^;r!*AYsVTCwIToJC2N}bxXjgS zc{~CB<;WaAB|c|~Vnr$0Xv9T7!~Eai;HsAyAC%ON!G2!AtqZps?99sKZ%Vs>_-6j$ zpK&W~Vrg~h32G>xWE8ORrmEYtd=rK)0N#Z8SuS2?%ls+j2L>QMw{WV1WwsMsj-|x* z;O;^E7&NjtEix~V(OwKB0R93!jZ-u0@eTI{UzG}($#a^}KeQ|?Yi3~o^yASrX#ZCl zNLaWMD|9O|byeW`xHXkiY%+%1ol$k$89h%(Ua2`N?)&|#^{FcJ;CJL`*A4y`{OUJ8 z1Mv{=px@fdi`jXye}OG|5cby~gFL=6jBGFVZYeYo)xWEwlpM-SPZ-u5piDx%8xh>f zgL!}TkiUCnSltD@QmLW)!k}M?9T|wW)1vml4f_j$c6h0;>S^27-*{((WE|u@**7S#Jwz+;kIge0UBb+j zzHV)A+gpg@7sKH@XC?VJwUeGDM07rQmF$mQcHalOl6Fa;e9X|JX-5Cg^Y_bJYX-8O zA%BTIi5hCQW64EM^6>q^e@W~{RvoeUuF9AI^RO~4d!b;dWlr=$&tUD^RJ#~c;_k+K z-^Tcc{W_~;(SBwqbW!QXJqJ{u+603Bt&~g~wLtapdgjatz}MeGN79x{iu|lo>-XRZ z?`M6BhVoyDVu~k*Lh@Dp=^h>KFi&W%cHy9nJHpE$dPmu-mmS9aaP9(q&at`byr1Wo z?}n|$;veztcrDq8 zG^lSf8F~5^6zuDx(Hai$enG#I8vt~8r_{LJ;QhMPX92?e=ACcw)76X1QNE2kd|)bw z-ymDE{q03mUkJ5-nZ1&HyeKMB7Z34!c)?w4S9tigsHw!UwN^HOSJK;bHCL7P-}_i5 zab-UK1^Es><|hEu0v^c$#kUXxCYijXR-vUa=PQg?MElq$`t>yHCzp>3>!HcA+X(~h zhkXTa6eAlGSwqDOg|J^knQa{|yNIFIE*;&%{O#N*pVqVJQRuAeX}CD$U!(S8>$yn{ z@}l6*jmA)!k4}Rp!^{m-@Yi;YoCba!-M?Ytkbiz#FvxXyM&7z*fEm*R^YkVdk~Wt) zgO6RiD25*q|0>wg#6D(ux4r*_v#>vdbT&e-TGu?@P|&RiF|n=hj$gR*{3wb~MwM~O9Um2*6plLknk$Bwk0$J~L;Zf=r&pa3-VetM zw8fg3)c2PH0IwZ`d14yjnEKUO2Y)}$%@=py>)3S*^osnN?f!$Z39Uw)f9oSn^BE81 z#ao+zk9W^`Nzr@yQm(M!8t{2AUn>Gb6l}wmZ-|@nT$Q^&S#!hXfjRy{Cdol=eNP^{ z@P``Aznj0htBG~_|MUNm`+;R-c#wAh&tx9T@mAqg!+skw#NNs;*cH9;$BN8%(9be~v$uhIT^rq7Bd{Cs zr-F!cc&`pIMYZ;!YPl@W#E0h5a2yUBGns-HVdfckUM`HFdN?UJo9Tu8)IuB!CT+Tz zkK+F6v_$?8f(WSJ9^VpE@;fgU<{3|fj-3AV`TB54+D5G@PnhrFU~?O-Z*^N$t$Nm< zY?w!J!~N)av+WFG0>n?K_wV$z#s6`=RP^4f3}L?O;cG^r^lX@2kuI+9{5&mlFZ2)a zU|x-qOAf+kLCIZfCCD%1V}JamnN^JD%TeNq1w+mhR%}OHTk5ujy^V>+X@w&ci1(pofjf2qU&J+vph-U_ zxZr}-t#<-`f_h0r{$-BL%JGhOZCPbQkZ&`L$Qad~cZp84$Q|yMj_oO(X#c#=E=Tpo zInuiC=P+gBStTAW$UnV>qPgKJSHKOGl9RiN;x9vAterJz71xy?w+Qh9{NIQ~WBOLz z+*MmXW|e$}&!-TT)v(RW>~crZ{TCFZF}Y}aBg$u1U&M)SAJ~uO z%gahODkJ>J$Zs}#{A4{g?$(t^;r_WDUux5lmO0yBMMd$D@6pw%Th-ML{I#@L=0bM= z^C12pkI}G1-^PRU2K7@}4Asf;&O&R^JgWB_-KIgmLE8?mjy^*;FI@1Ctg|L@2wH!N zLB0<9uiN_5O@!AU3dW~H5Pl-zEvd0tF*QH7FJI%S>uBnHk9e5_`TiTf3Jl`M8AN5Q zKVOqYA9q-1^5J(bznZ#1mCSJ%(;pS}38qak35i_llM&`su~$ zh}PGJguRa~Zsw#+)B=VWbCeLyPZ1oa(sZT|k6h<^JW5#o`ak{YFI{Y5s|4)#IoKZ* zf~kVRw`E#@2dbgpR?~*)vc8D(7#FNCH01JW=Sj%NrnU{%dT&DKb0N9ZykTYN&d>$} zQ^;(T57q=ieMPWJd)XP0d%GcjBJOv#a_EHmOzre> zLp?ZO+?pdz4Hoz{$9ywxiXc98AJFe(%|6S6!_``0zWaV+$04MjsQ$-CP674n@%*_> z$}xSBJROqg6kkLo|2jEAcVDN%oo}!|WC^Cx9S7=n6xr^Sh$l2Y)maPy^NeWG5 zHYu&ldo${J23l;2H1>PunUhd2Xkr?PNdHD+i78mUv^gAH4Bq zWQYQu;?}rpQY~G@wO6d+a;>Z(-b7cH6&C)ai+u3lp?JFxMGeaiBBG@51S(~OFYht0iC2X!gy^VV;d?y`>XiMOML_s4)IR4)+1A)0?`?GdE28(C&s;hV zQI{W@*HGXI@sv}!@lbQAfkMo~)(s_DM*jdmUi3%TqOW<92c_uwNOVNhp(JEpK}MH85R_>fs^mGJX9|Er~lTfNwDDCF$iA7QQ+Dkngmu0O=>8eqmFz zDpB7&`LYGXmwCaFfM-%ScAh?Ft8z>D{^>TO4dr_!{9j7t=bHq*5cdDh@pv{~{}^=O07=XXFg0Qc;*E{)d9a@6vTfPZ;f0gSidv611ng0w zKHUWUNu(L{NAy~(Yk)cBMX z#e7IXrRLp;gRSf&{xCSoLB4Fh#`~aJ(QDi9V~Zq0@{#m}3;)xv{8m$|Hyj1;zn5X& z+ht#NGs(+&Vi47%7!lzZ_I{7*zQ+}EBT_Jo$Zkgx___c3d!MYBFkao3CBN(Jf6%;s zKW{> zq&Lf0Wsi=vdnm&D?%?cm%02Hh`cgJ%_!S=d58mb#B78n`b(bwMbn+h3k2Ua&Wmm25 zDOrg3th%ub4j_&6sxOY0y$9;|tIcXd0`gH#`;VeI&%I<2KI=)8Ov zIEabKN`H{_YNvDZ=OY{PE+Rj}nQOV%y7iF$n%8;2#F|~yzc`d4h3d0>10`^ZyZ&eT zlA3EVynjO??Pki0czdr)R;y7z6(jIyhfSMl&fRYpV5uSBd-{I2zSvF8AAO9a_6qfYPZ~SGzlAVYQ`y_or~g#m>-WEZCR}d*&+qh6jtTR9 zC8|He{BXv}&Xg?E^Yyie&ld!QlkK{HGM1)SSGP{Xd3Y`p976dN!!=sigX$SIPUGLd zkF@+baznk*N*(a3{j}|F!Z!Arq2r72yaGl5ZND!06UE#4u$RMfM~%mquLi-u~4QSA9*qRsnW-$ukb5gG4@gWeY}-BL?NhIIB=pMqzCjWsAukX}n6 zeKNiJ?)>wJLl4iVJ^Tst)X1HjJ*{-uk9pE%c3KhzNzv78h`+rIM@O$SX1$W-&W1m% zRs)}&x45t%`loK(tU$U4`FXe5L;Z81GG1g6;#oh;Q|l@%Q&^Sq=g+Dow0?#L8oRDK ztgdPMdQb-RX)TPrpSF0y)6mssPke>_!Q{{oYg^{83~yE+e4p-`bD~=x2^@^SZGM*x z?|VXUSQ%S*?J>5?DUMV3@mU2RSb?>J@6)G+zy6P>E02eI|K1WV3ip<@7ebZ{SE5LR zHe=%2#@3a(r7TT_kiAtt0P5~Z-`lnQ0ae)Z)5JGu4U*Olf@BZTAL%$rm z9(wxty}eOd`(2@4y|^BL2pQoumez>#*5X#Duqu(Tr4#W1K}0(@;`)}D(wE3T3xA%_ z+HN~&BAsZ%66fzsO>XHH%d*;`LaA}P@;R(|akPy{i_lgPqHd!c!am~U3iuwYk7+Q~ zb%4ykEU;g)DZq}^^xXmC1@I$&?DA6An>7(ZR{;OR&&SiZxJ(Xe5wcoXzM5V;y8;Dg3MkDCDX)4_|4A#Pq(IttUrK=0N}(bj}Z|8Y#`M&6!> zu<{l&nGF0k)q1=tv>y}C_mLWHxnADp(~AwpyBiW}^GZMs0Q`4Ro40r$u0^4%N(}93 z!cFrRfLCC@VWBOXtw*S^JH?Bz^lB9EYjP~h(~gtYlmNc?)G*ekp!>|5NUW(XvYE{* zEh*#Kf*$jHE+-&Z%+H5={1lLQ_4~XGH4(r6x@>>Aq=b}AdRpogly9(ou&=AAvz~ji zAzJp=$fl#nuLpzvS959fY{PBGtkPAWHxNbGvRh42{T*?TO&}0I85*fE1#lif?@O0Z z_a-Ov`(9d$^{Lri&>6D6)jL*A7V8JjDVSc8SodhzRJk7Hi*7v3n>%k?^j5!qDhv8w zgOrSSB|iiGmk5?p6n#O@eNY+tFEIc7rj?-!@qZX^nn$lNUj1gEVhs9)Fkj>O>c#!X z11}$Gjv{>_#2?owjt=2v{FX2`gg+=N-vv55uOzcR#{&Nf{`hYi_4M@CDvbVmx7By{ zYE5UkXUT*}$Q*e(VJOZonvL!4q}ArBl?Mk?T-pEi`<1l{5^&?<`>x`@wVg4%5fgf2 zfOlr`C$34-+M3pVe^xpUd;s`M6%FS~oWFA3A+w@EHM#!q6wH6li10p!A;l>Fn#bZ2 zXfH^0J)a)SqyDULyWIP?w2_EXuW||0zluoUV~-`%J{vty{RQ;XgII!oRjIAaU)N|o z&_7~+HYf0Ck`jOZCF4CVjq))oAy;5(F*ti{$gK7ym z8i&Rgtx}F`J=&&nQv`ZKfmg94ir?m;^R{LBYn5hYvzt)-pj?TOt>~cBdp(r+h~JD1 zS@?EeSY|grim7ckCYgC~k-ksF-0Zg=_$%-WXGki$L|$LX{VluI_0Wn#$M!`n{Fu?X za6bI9b51qV3OwGS$O)L>T`#~;ezZ6j8y#KRs9&=z{uJtu)jhy%QK_(1)gXRmB0ecH z%$65`Q5Wy9%9u9!@Fnn{6R8V7gp)_sDt`DSI{C(AoyRvZe^aW>^+M;pCP5SIm|rPY zb{j+df_a=!t+EZqCJKX_PW+vs!1&KUELJ(So|BYvvi?8)xRzb#=lyG;J{+W+piNH6 zIwf#qzuy9MM7x1b;81}BH zDBvq|v=8(a2rr#4+EO=gZ`kJUG8Lr1X2E_665Vcn??lqVIPQP(a$x)9!ih!byo!!r zdm2I?+M(!JVP%N;;482nnafU3X{&{L2>cn$^f{Wv&sX0{sm?|HEn&Rc1h>RG->x7? zC#973-{(JO&M4_w4*7l%Tiv9$iIb?}>pYnS_aF90Pdv>_Y_082)bN4+RFI&Vc_fw= z@_2rS&bDaj;E6}3cOj#|;vJ!zzWd4(yL`tBj>7yH-p8r`H0mE?X)&@?+E~`OT!lW; zo6N-%DjBsY_^eHA!28huGHn!*xY3Gy$MqHCQ-m4F2NDv|cl!rV1wcMV{3X3Cc}!~K zclYi4W~q6*dkxPg0Y7p9M`SC>x{!@YqztH6MRjii>r~MG)+|N$NFXpSwDBkYoeu=P zk?l_jW?#r}mNN2GtCY_=_GCnuimyCWsFYBrr)^!?UZ z*f$(ObLO0ddbAfS=yc}f#kgM081hRAZk_p;@3hIho4d^!#gjSTXS^Qym$Ia}4x$6x zSJ7qr`%4nL*FJP^D2MNtA5rkGm?pcI@>=*N$ET@BAGEZH% ziucJy%s=?qSk@@-0QiuSZc8BYxt+r6u{J5Gt^et-(^FLwO6yR4w8N7SNg^%xj&-q? zM*bsMC{6bO*F$Mi)d2dJ6l1{yPOGcl3&*cM^493{S$O1{I5lp-k4q>XM)613&F8i~ zI`mZf?zj&8|KDb2zIxfW@1j{-<$MsH!=M_n8-#9`FiWuOx6L2cdjx5mp#OYx~w|KlWKMPJ>>Te=V~O=J7=Yn{l0rZ6e7)a;(1)A%JZ}R11=>W5^yajpRx8$@?L~2 zGF;TBGJx;{GnUp6Q2Bjxq@>>T_B~!Zot^%zp95Mw%^;otpP$#}tk%OI{|JTr!I@!t z!ZIn^+tqiE#HT*QOy?*k8aaBo?_H~^3M7!eNeFv-a@22Uf#UkRJLo~(xL=?i$C$)E z#uoA4ujX5Cy7a5`0V^#5%+J3lGzdS5zJD8?DX7mO-te|}&{X01 zeVJ4+1bkhWevP)9bBwv>$>B{9ZyGGZ{9D@DE9w;~Lb#94Gv=3FiOnWy8gh$=`w*W{ z=RpvXie-y*A2>ig4Sd&S#psy4vNr2Op;xO{1iaEU{AK5lu?W`C@55J--T;gDBJbU_ zNiO|8CIJ2I81_K2{HIzUOK^ICOqEF9zu7P|ZBn#=&l`PBNU8YWJYJ0a)goR-i!8*i z;|a#32#+lOvWQ2~&n|7Nhxy+@45P#>z6kn%& z4|=^Z(f&2jAAiYDuJee`jQgJYsPjKPN^|7RC4jH^!f*nsLU*`{!EN-B<)ss{POgGe zDvH$ogO2cC5B{Lf(cK(xPUWe7>G4G9KPP_^nn?cOYF$zxZ*NF1FY)&!4uJmGkre3y z^%)!OH+AAGCUHFsLR+pwd>|_5M!I1!rNYidpm%Df#D{m~#az7{d9YuMC-D1mn~J9m zHh1PI0UrxSkOdLkT*8HMc4dtVw;Pqr-Mk4a@6@_l-S2nPY+p>$=7aIp{VlPmCq|*ECx@#eR z)LmG-yA;LCnh@(3m84G7qGj4~==li4^&6*1KrRY~43Hlc<+lEPI@giH)ap5f;)Q5m zo^3m~SD`mYVG`9(tn;JJdhG0nanoL@;`%xvm+MyPx@$rn-y+tBgwRGt%q=UDj(M?A zykmA=qm_(bzwEk0(+SOkUz^S!Pl=78(u!MF(O0kMi|@~E=^`VOr*U=a>?F9KUzvMkBo!6q%}bKz{lxhs z6u$2wf^|@s9w8;@fBV)X(;l8@2O&KK-gc3MJg(uaaFk*95aLz%30kh2)hO0^TUi6* zNdqMyBs|=4MGV!c4)ngDr>%PD|7}^L%_#b(eRS=r?nzjxdNCF(zuNYQ_Ze*_1!>Nc$}Bh8-9r7SkO$2)Pm-o;b#H3@ z{SrTOF{GctNy(7x8?8n_F1c$2-k7MmQ ztpqD6!uxY#KUlmiT{q$BFZgjn%`wyu3lDFtVCdfWPPYE*6yzr!7G6Xh;Z;CilAl*LPXqkk#nMuzjbW?TcV;H{!2N-F^}tUBiK>~m65p|(zBf8i2YU60 zu#h*6&CS11jbXlt8|y6A2TZ&-xg||%I_hxZ2KpIqqWo)|a-^M)(|+&|r?7BaXkp+V zYzXyo1^V}+fqgs0^E|QYMMTcxRPrYF#5ftv*DcN&AKXyGeZp5m@iW3-|M8)crw05i z-6KdZGFMqTT@{mVY}q$0Z&}bN_6rQ8@Xt4bpFYGhj{G{)q>fFKy8yrT zQW7E$h6g^#mavIAX|}%Rx*qf5{5#>MHLDm$&4;DJtqwZHIro1PF`3?S@(_QU@t5U| zxcn2@Z~;=G{t=j)6Wq9MDr@<_CO5&~gZ*i)Q-K7ld^$@wXsPyC>~EkKCh1mC@&HRA z{%;<7uBnb9oUv3|CmEQ9K%S~1P=S6 zNveKCpO~+`@}roh7ftp3R58(QKqd{gD{~=gFi-7mowfAQ&zu~D{eupwaiYN_cSGQp z&0m#Dnvi;^Nk2M+Zy6_pLv$wiDTvG?+$xu@8ct{`*wT9z;ei+(&IN>DB6fh^-DH+x z5!=MR#Sh{=`0WIe#)^X4ZLY4;h-^OTteGenn|X4PQcS23i)GQ*=4E{==J78qP|^RGHfooQ7s4j_y~mx`PMAe`dV0`?=75UF&TA_>~OwE)9;Ttod5e1iTKw6rbdtwL~`5Q$3hpU3ZJ+eL~;F#uSHUw6AXUs#z=}Ut>`R7W&uB#lxLN zlGXThJfMlpv;fBwP4fAugyG&WW*xM0Z?i1AZlD5<=0g{gx5 z8EFULcPQuCQ&Uq)XPD$V@~}qoUhnqwvnZZD7ttlfdTLL*%0D~y2rh4_B8l;0;g6AT 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 puFVLqNn|t%OjLgR4 zMx2yQ2yYPLRUJr#X3!sS(V352ke2j%q$HX*M_7yF|K1L)&nqK(p z&{i(gY#4Lity3vvKOq;tHbv3vsnL$1KQ~Q)Kv%YMpxLcq)Tq>1ilQHTwO%n z=D#l9|1Ym()bFUt=$g{Z<5I5naI3}mmPeC4pL}wYApc0;6Ft`kz-QomiW+$KtsLlo zbz@$Ve;&im31kONED#?aU&A3hQ0-Zp^lGLA>9-lh&JR#`rErxDx%;1=zng%5 zE%4cz*micZt9<$FS>VHhUc+xb^G9Bw5juY&s!e!zXYK}%>E+7k{nWX?t5ztodQ>pl z9{~LpF6K#ctde@I+3dm;p!^VX;@DXhj>bpC6~~(|DSX-Yhru_{uQPl%JiadF=Kt;A z(B??8jF|{;f_yS}cvgaaNA*pr$(DCWpAM6=+u6i#nZAhnx0t#f;p2xN*9glQz6k#S zzTVD?0A9=Bg@JCczZvo_2bOKOSxx!3J(I9+tCpfu4EX`*iKveTwmQK55~3RGbx-`= zaGG5V`j7|ju^ssXFi)V=!Cex%#!X{+ZQ`BJb#+|R)%3l%Dj&6&-9 zzFfpl5_!DaQX%uF#y6?5iD&s1i&4K~>9~w=LC%SDdmI=0IZpe65x37PTSZGizZ>*X z^DSty_=;seO}P+%rwucIct1)=t7Yv=tPh<@Yq))c2YO7G_O?Ft3j%%+7w@gtWbgRJ znyUi+AFzjrCPFvN0~z(Qd zAfEv~r2|gAn9v6P&W8!tE11jZViR}zX;{h?eG}im)2k)#yM7hl|6F?#{QWk&cE`_i zqKK#GFB<rFIpwmptfd3s ze|pJ8$;ezPPrjjdT|LgJkNli0Vv0ZbbAFwd(4I=^8N5bnUx@lY zzz-=o?pVE)d(wz+zW%|>l5O5TlcMQgk`g?Z#cNz@GgdiVotwCHDQ`S>_=|s?wN?6ac&|W6#()3%bsu_vh8Ia3soc4C1^T`UxIezg%hx~f zrRhp_x$#4Yzl6o{E#>npWlra}1fzHu@!G*FkM?w7cFJN&lTjj01oke^x5fA^zsjJ$ z0{n=K$_w3DEWWa31*+d*A2X4_TTtQEzFRQf(L4?F%xcRO9Ms-M%5oup@E_{#uJJ3T zQ42ad3()=HsjhUX3@hf(gY~dK&0W`_ebsxEKOFH4-w5>(?1NzJAh7$_Dy9~E7sqce zz<2wk_PuIbcMI~Tv6}QI6T1A6S(@zq6DU5{$7)llZ*%f3-~W_F*FQ&DYT@7Cbo;s@ zXRA1$${g_|yj)yUE}K(wbliSF$yPrN_zt4>r={BB_@h6r;6{+c_K{P?q(e?KMVaZ z^T=@*cwWnAhTen)vkI}0k2fLTf_lr{f@QhuuA=0E(8=4owav3t9g>X)eD}MyeYHdLV51_29K5d{GKJb`N{DPEXm#g+KM~A1kviDVH+dv; zP5u(w>nbYyCuXC?&q}NTe|Uq1^#fxy&?qgh${XyBAD$^_8d-j2m2W#`T7>n(-UrizD&gQ3Iol^!{Is=J^z4*z%U+( z7Ma`9kF}IlXT45Ll*@m0jU>*$oevgQ{5q)apck|g>9N#%G*GcU>v?xpT=fM1^wX&K z)%z~{yUf%(MbQ7&>0?q|i4QckX!Me$sK37$UYyt;>sT&W?VHh|O-V>qOs= z>Ejlb>9(&pFx@AhXa&8ZKI*cYoCs$Y>4v^V`7+E0^9HEWjGIH}Wud>`L(Z@b20 znOZH(0X$(0zZ%eu0N%1$A8r@QKf^O(WU1}%?k^>#{EPBMkr(|hjcZ4%e7S<~=6&8?Z>PpYigkC%+(bRCrsZw3Dw2KrhW&HMH#kW+QU@y8wZZIf=W9V^N!)hVrCP{?2sNw{35glu*3+x()J)F)X;tb|9^f zQC=ki`vpDT+xf-OI4h}07txdnuW2#zN7iphzXHmfOClA*Q;YTPz(gTNZgT6^1bgqr zGT&W%s?0i+!4LLB`84Jsm1`d%zm#vi0rEk>Q=Rkj^YcF@4Sb+6wn6-=_t2(B=AAR@ z>)*N@^<#}#5v3SSe@1J_{s{X1x(sT*!hi+itXu0D9A&k~u)r7eJCZly1>e6TdouDk z9=QKO(CP@HK@@J2@a8FD7GMUC!}{MOO8@bbFg=IqG#?~NqH!k)&|iv&{D^$q)|UF= z(yK*cye~nk`@4Vl9{LOQ%h12&GGFOia7b>OOl~R>(C3S&UarlKT+ie!N%v4c8z07F zTd*bf399ORk$x-FNza1K4mrs^RR{J3dMliFmTcVT9jY5HxFh=|$NCNPXmSkSvqK8V z9t=qqEMjJcviiQ@_MR0%`&~HTFMv<8tT93h_Pa5>?YGLKd0d{nUbQt2o}Uxz<-uKC zitm8{zBr|seL>K*q;U9tJC4tr5YMhO>226$!~KXaj_fI8efGIL|@@dA0=)$yM>~Y1$=tp zWwqi761<-w1EQ9k+{W|qcSd@%#NYqW%2vPg*~XhUiw;2lbFBX8Y*^?A*6y>1er|pQ z^%yq%F;-II#qEFNN60S>(?QbFQ;JHwXuoO($;eSyxm-77#Z-2M&B+s@U00{v|h^z*JafwcH6relWc)c2YC3Llnztd+@cM)A4s;z55`@?Uov{!>*(cn5Ysse{9(o2;t>eZa%e-*_WM z)pYY<3QpXxf%t&tQQ4d+#~lj^pSjkMKZ?AysK+>I(g#Ml4CL?YN9eogud-;p19#6# zoq9<6XR9AJHk}+K5=i#L{j!)2?Ng%xjeS-PjxNA=Xkj`~)#!uNr2g?TKJfRaYPNoa z8D**m$23j3dLUOmts+7JGBEWBu6l;odF)!Tx&n-PCW2>08mKPY`*WGofz4fcOL z_qTD1Dh>Or>j(XpVf-#C&2<0GF5l|!80tTWRJ>o(syd3lEjM=Wd6HiX^V={__Up0+ zi>D6t;s}zacj*Bw6R04`SSw zkc+}kQIV2m-s$kXVSbxc%P9TOY-hh5>}e+p^tgQrmxjsDEpUrQ_BYpSY7-H$h*qT{ z@{7EkXk*O#fF3V-Dep7Sdc`~zQ8kkHfOoH+zr{;QJTIC6@wCKTg?KJN2IUu&ENkTUj3qsEwQk_2mjUy%Pik_ zrOIAj{R>A;#kJzJZFLFEJ3LL2>qwkwEP9*wV+Q!=qDurKdy^OskJr0samBtYv82@s z@`0h6^ytXQ7>{LLHkReNkGWXcv`g1_USpowNZ}t(dhWlZo(qF{xamu3#cZOOFXvPo zXYQ7@uzG4P1m$<(Ua|w7KmWq>EQ%J3@xTnKMfcOSo4QUAlhiab4`#JEuKzKO8Lxdp z$)(7gDgMwFYdAX*#59Y01oL5V|7R>Z9^0{sN?p!isDEY{8(^1j;gtFGIqgyIK0scl z%T9J_>?ba=r(%(bw`^+-e$T6HVFg}Ze2`>A>R4qY0K9FawWl5YOD|SiD4$qz=e)sH6ZmDokJj5!aV#fIS>nYkIuGH_Jadgi zhZT|+g6^PxTU}T&wUV(o<>n}p5xMlzatE0#6wjDazudGyp9AJWRzSWwSC`*3@#J<5 zwWqlBb|CPj!ZjJkR{;J2^=7m*<$b9}--mmGZGgAbCqzXJVco@Hd5eNQ*TgcjCwISv z`OV8wGuU!?9?*{r@;PeE~urxG;(Sw#qRgL=J}{MrH%-ut^#=glDg zcJL#-lCHBq{cS+Ei^vr75f4&-YS4~qFiI>%5bwvx4pw9mRp3Id>R5*IKj`mic2zdT zT>q2K4?P&0?{RC_S+E!5zvFA?VLlf0P>b>h2R2LmDX|px6DGX*x@{JHf5h|lE)r$6 zZ-wU+1@s`MpBiKgz6bv%FpPaSOnk87FLe`h5z5y^R7rD={-SnYqg@r06~23&+Qt63 z&S&(}vNh7a?31>7_9&jcAk%>*IM1zz{<+?=QtM@cpDf6CL7!5p#GdnzF1)>=4V_1$ z;_q!0YSHV0r-F&Qd$VnF=uT+^kWXqiz1t7|U8op#Z$`sc>;ReNv)qdT&=70+#qUROvXu(ovbJL{PMchIDwXO>A z^75~vZL$?AfERJ=y}t8`AKokA_nMnq-vIl$7=FW%_)b(z|l3?Ou=&;2Y!NUpwG)QErL%s0Ka5|-rc+CRrAXW5Buaf z#`R>s8+jxIe=dw56qhHWeHh^vm+qi8Wo>9zBGy{pfcp00X52=#<(XQ9^QGc`dk(En zS8j=PWo_6kRBzQ~Drt-!+jk-4d*64+H%2hr!raKj59$q{o_a(53i}75ais1tS8VFW zZ0iKv??agPw_N(i!p$n8l#2hu$L<$M+bB)U==wf&>5N&+ALG}0wb|<+dow6>;BSdK z`!E9SC%1>6*Qpf$B3+~0)SC9ShIoSyQcbKpzSRXYFGvcNj(F*5S0vU)p-1L4p?oSl zTR7!wDX+M2S{3ORbB!*}iD=Pj(uz^k!TN_gZ;ALkKy<5Go2!Fd|8FE ze$5xaJ7#CQF${O!_U&v-85ORN8)0E{0mE`mgqLy|+u-{|mHF)s%{t{aD~0ip-|5c0 zA#VqTf$E`x&%vmk6~%uHC($EU8}6F;Gjuo=`VpaGy+d_&6?q2uqOP5bpgtEp&G#3% zLVSjJT}5qT>8RhCSaccr6Y`<1_fL~elmo~-$X{R{_u{LCE$8t9@@r+trfl~J0u>UJd46^CDBe_QME z)%CNC-(}dBY_~TkQX17)b+M(iJc`_nw%#?s7kl%yrYc`aLGJdx17-sE@pHzozXJ8w zqn2ygC{Lh#)8NgI!1U-&)kte2;0ILqU{{Os3$#sR9-Z98faf(v_DHU@WycpJt#_In zw_+fEZe-&?CnJd_lIdAdSd+i3Z}Q_@;4k*23lXhq&_DE`mK^Q}F&rmXA*$!+f*s6r z8}AfwzMYYQ{WXk}ogN*4pD-5%Iy{B^9_A+%aIgRz^t)*#=y`@S6tpdi-wq$%dSPGV zXXn@J9~DU>{k(gwj?nMxhI-p~qOWtXF5tm|h3NAm9_}+`x!9#h-~H)_`nTaBO2rS{ z-rWBn|Kvoj*?Vm4O=hpcl9J;OsZ8-c)^KOmqfUlxyl?hr$j?##Pg~_-HJ71943JV- z7Z!BSDJhHz@Wv4S&lGqb!1qg5BRpOiK0Ufj1k4y7J{-egdNM_Sqm%y^@-({fR@RFkAorvbC)!7}%$e-6_7E#nj!)>((+E0u> zhW^uyEd-+__KL&9X8`|$`DpK;Q~MvVV+IC~Od|g+3<~g}%imJ(w9qz1`zy$AWT+Ky zH_*!au75Xo53(V3+OLQG<@N9K3g%$`5cb7$SQ?zp807^Q0zFqom>#o>c{wMVYmhO) z9#{v!^9iHlq9glX%N|quEIv=6&)XPXChHvCPK)QK!abB|G_3_2K1i;RxV_Pp&}Byw z>kTMKO_zcHgninDn^`QUbNyc{3BVt0!GauYrtEKc`hI;T5BUlge@L~s^Ov2h;2*7a zIM35CUqoAS%>96=;|0RAoaJhWZM|73sPNLPlr72^MPc+46Ql+AuWNJ!fIn=ExK{c@ zG;=4;_pQJb>Ny^>IvGbjep7bhk4+QedOeIq%_nk~pWC+dKEw~cVNozWU&0}AVr^Ml zEA*3$LIi9zHSMjbo0BF|j{M(#+`S=woJv{P&kV!b`8=GQNt&hd84>#ax}tm~HmBpx z6Vp3ydxoPM@^yG@6d!4WoTb(%euQh`aDh3h>X8j|Vt$o+JXQZfIgz{4vUoQyv^HNI z=L7Qu^9QN(66pGb7pypR$*US~?=;EI^uRv!vf6LI=Xcru$UZ~~{{D8P4Os-6aSZV- zn1q!$^ULIYf2~uQfO_D9fQt`kZpU3JFLc_VSgZOd8=e;pM;9X;Ql1?5ue*@GY2LDd z3%Lss{$mguop?Cn$bIQ`k5t5;*QiiZ5pZf5M~Z#mS%%GSR8K7nIsEX*?4_nBOA2eV z&&k2Qq#4n>pRf-CzMs5jJH>?M_Q%qBcDXg!n}IjIQ(xCaiThojfbeC(n{yde@73BX z3yy9uaVPf90zRnmMyVwC7(WE^x9$jU8~-=mU$R?Y8b5!Qu`T31T<`hWhuk?G4Df}W z!_4A^8hVa7e%|*_$U(j}M$RG;o0B^WWKA`p{{sDHD!ocq_rnmkh}nkr`Ctw)B@UwQ zwdL~RkH)Wy`N?@+;T!SypZf0yeLyZ#)k>B+dFRHg-H`{$N^&hSY>X>;nqLBqSXoS<2&##O|^l6|AlgNO=~;ov6)qjKVKk7as#>v#zeZ8^Jhg7!>{7c=%W`DXOv!pMF|Geb9rCrS2rM2~~a^XHn~8xSw>vXj1-2=J=XjGO(Wt>5F65 z(?KOKoL@Pj`oZ8(KwuDlP?aE;-zno-oRO*KSI3>I|HO=yHH7a2J-2~&>h+DX^Rfmf z7VEl}o@g(!5KK=ssJjHMg#3QL%kO-?E@p&#U7Y`A=Buk~NT!g!dx+pXval*EJAY#1 zZps@^xp71ml0`S>TmU%$I7!RI@S-)T-|d+Z)g z+q3dr9Fd33UJR<(5B*UlzsAFY$EINs)lo8vaDIWm_T!42+@awUTmQo${KhD_l*b#h z@}zk5X?QA!9dt~M4TO2V`u)Ae+Q|Q4!TDFyj(siivo0Q&*%Y6#DL!~^Yz+3rgk|g% z=a<(k_Ulxo>B=tn+)r6@tMs!!Reo@Kdj30h^gX=aCUQ}cZIE8m_3zGxfKPSsje@nc z^!8dQDc+r_MtE^Vc9@$ZiPxX%`cF6b;}J~12uE$)U>4xJ%%n&12JxR|Lgh(VVcm9F z-e4Q-W3IbaN>y@nxyL_kwyX#AUP+x-0q>iuBZ_t{LHGvQg1*Y$@_DUq6{!x@+jR*` zkC90y38{|%$e?_bN#ziTmV_=$xh4taui;f)SEQ()WdnwD~$;QYUs zO@#crlrYfic^n%y^+6KNGmtM@7ACK1a-eMdsu8y&>*ZjI!MBOIUvpPJ_H2OX*M|?% zW^?S<8wh?CE*9TEq4;7q>F;L0!FG4B4?%d9Hcc~m-!}I%7@S`-;NO)qIy=9)`Q6Hp z5cdnrsEsZztk-=Yiih3@xmw@iL0;@EVL%WpH~7W+NM5_k36f{si{F5eK7` zXs&f5iL%>Zf7P79mr^EErvCG~Mp?icTCm_~s=Z?wbGA6CPrN^a*>TkQ=^@(P-prkA zp5!0Mp$i{>7tKcqo@Qzz{~!vcs%x;1-@j@i6KRd&FOg$MQ%p}){BwXy0sT=D3Giol zp5~-8tj5u3VpjYvd*Hi@Vi-@2pWXe-1Jq>6n;$+r*0j(~am`HX!L;j{1>Z%&*<_85 z3$0Mk8SP@&J}6C-J{x^OcK2|?OR5ep5$RD8%KP;nLA@jLs(C-ywTWU*);HHeawSR9r}kK8j)R;f9tC`{1Em}NoM-0gj6VPil^pyW`oMi_H~b| zBcMJ6zO;pt4N+^WA>*#rX~0uP@WIC;>}-ax@Fm;4o@idH;o3Q73#UUOqAK5Ii|Z?I zNhQZ_bLQFgGY?I+yuA(a3A1i`)AGvkp(FUOAtQ^E5;7YzivE>r_<;N~d6g~8(?_z> zU!@<;5A5fo(D!dCw9W0J`$9Z{{kq-MVj6pKYlg3ojPP1=yd|w5`9qn|e(Xo2^xd*O zpl=6R1X+?6=z{PPBhcGbi>TS5aeoIveEkI8z6bkU*?cWFykAGOkGj#0uAq8zuR*4D zDV%rbVv7qG4xg`veNyoLd&ve`R87v2!=DeorJ()_KBkF9w3C12TQv&y1pTGLqSA&+ zt9D6^>e-Zl|J(m^IOY1NYo_&e@CO!!xE)XBPLzhQZVs=A*zaod1>zZ2-?588L-m(I z)#R2$>W}Jg@);5c?+kYWy&T{ZDdw? zXzg?xt62L<5A?w@Y|?$5HlX)|s3)MLF7>9EiIKZvs< zk+Y0uO9pCpgB~BXY#93AqRXD!U4j;$_J#cmt1D5zWNP-zRryP-k!C0Aw1g#%!}!}vo~O%gwkUZ0KTF?X>d zyo(_o7{~^mSbcOn5cpo?{S$ju*=s+!t>=T@7k+h2<7m~tPk%n(0)GTQgPGx!k`qer zGmgsh!5$mQK1nzd=VN>}B_8to$31h-VVr4folehw!xC}+?Dvc!0%;rGYDKY;Tbi!1LKU62a;a&Ufu?-dVX#+7Nz@4ki$L7xcp zaFdcT>|WY|$gkWrtAh3%zn}*Fz^R6Zj@`J$h@Vwbtbyi}n*)BS%{&Puq?gG89xW8v z*|DMC>;OGfDv@?QjiMOoSC_l}BX(nKh$Y}@Ra-mH6;LD+E0keh6ykqNriJLCenHr) z;N~lFt+G{j8M2T+?h$okWcl`d?i`gGvbZ;caL>C({Je0EoJ92Zpcg%=b){!(<%>O0 zp~|i=kCOBY5Z-(J+1%dS!=Z!?-yMGTLO z!=GJ(^D3WcGj?4QbMN+j=!Y$p=oc_0j89Uo9nYpm}16 zv_!lry?mcqw#oAJjsxiY*A!Z@SZpV5MYJl!XTTR6UVrlOd9I&&wwH@t z#q>T@uNh{bdDs2xpGmGSn4yH??`rX2K76{?n$KT0^T(|Q#qH`e!Q50Wp#E;i-^2dl zPN^<~*HUMmC875brjL72(6F(_mk_Y%+5JkH`e}@OhL!d9bpP^?_{!6O2Wd272bJvjnA5thybbekzr)NkO%&GirL~s6 zA}-)5)Acb@64BJ&Cnc~i@Y(4ldl0`)0!ulYv4Hg!>fteF!gs2Kgle(0$xm@VB_dnW zlyvNV+@mQ=Kcw%8bwyKk=Xf^+F5Idf@IRj)c|m?J8RHONd&!4NTdg}djJEk3p1FqrkLf;B94*1_en>rMiu@}xSevgQ zVVyH`lU9TB-(wqRCRoD!G;>1_79xS{CEPix5wvO!F-GMZQXbX+Pw(PHSmrHd_0SE4 z)FQbvp9Ww)prdL2^6p{hL#_&{Z-u~LYYjBBg?eik^Vny$T^+QgOO@g?ObUFFKM;oY4t82Mc=^58Ul_NfaQ}v7P2nHsW-by& zv(K2EX%_ddkDm`|qP`7if%nP7lVYwj1{>uXjxxs)KN0waoX6_&N(bLhTDG!D-pPxtnZ1BO&D4q!|iW`y>_7!C7IHjU_ z@rXtL3BzXOZ}1iGG2r>plOz_**ftoih63uQQ(QJ}5ngZKdcSja9< zWp`tjHgW&6?oDrUKI8{O%squyu3Rx~cOMg!fq#d7OMXQF=k`7RKlQcVb;l$TAGce$jg1xs~ zX=-Ly>@lx=CD=- zseJHhb8qtZ!Qmpme6@gvv9VhHk9Ni>=>IWBw465XLtg49D#Sm?e@?^vL&LvnXTSVZ z5ZiAEqk~YKkfzvwK>^t>`ALMHuI6x!ZOH*Su|D|KQZ3G^7xyEHYck;dw2+-ycJ$=0 ztKKe%h}i~sAKtlGch^{p@sswvIv`Bu6=C$x=@~JzV z`s4in3W0j>jeyMJyVBIYm?%7)^xlT_&^#{BQa*J5h}Z5iL-K0 zx}@ea?Okbtv7o2M`Xyk`E&RIdu(oy%Nl}vZzHX1Ing@kpX*@Re;Qm(gNXU1p8_BP5 z8Y()n27>S#4U0iP%F(8~6V?A0`}eLG*Qx+L@-1Ei3Z>lDn^v8L_cw&8s=9>C?bn)E zqETVO`;G5jR8k3$YO*Dx>t}iuE6M2fQr`}CX(0Y%m{(q3S4E>is%zs(6c5O|l+?Dk z`6IFO`Q5^7r+akPV;Fi1Z^_Cs6Z^Lqsgll%y$TcZq7v($5HDUg3_lqCd^&l6{vPqpjL2Aad!I0Ik(-7`O;* ze@C@8l!^&lGjGx0&>dS((;H*R9x)Xw!W727-sVMbK+oIIx$uRj-O`N|)2%_`dajtH z{vi2S*7bh{c~GwjF*CV5jV-IC&F4e{h*yG|i&`|Qr<*44Wyurpmwi~U{^Yo`1aVU9 z;ZlT;3&RR48J*@ExC7L)sJ;kK5AJBBtWxy-$8Z+qo6L*ur@YvNqZQ|f%J6d^Im`DY zUGr|eQ0}S(%0D1po4Ou59CLo;9K-{dFD_(Uk4ZL>9w=0T{_TE2jiVA(>A8!WpSc9Q z&xhU2cutSAQ$+`WsfeowL7!@@3;K9@Lw|?p=+RD4f~Jt zm9A(=<}0@ChI|n6uh;L`Q@VqdHw(Tj1N@_jS%}-RGw`@ecMsjO@j#H7V*uzKK>ay; ztXdQGuc3dUvAOtIHRrwdH}EGz;XyJ|k9EMyoab--zkKaS_CO{%V8^LnkKF(K zZ?v@!!b9lP%43nkePf>I5T6>4_TkY|{!I@a%0%@q7Ehv;X0+Cp-1zlS++PoQm#d!l z)$m8J4Ak?0e-}}et1ejVP~N&NMBFd);<;2&tykK8`RXOkr%3zQRrG&#h6xO5^nQ(s zN|jhX?)Z#8B~uiS@DAEG{x73cOG>{w?q2esVPu;Z@QbI7+x(r<#J{hJr+zuon`w|9 zoQLLXuvn?wKJ%=519VXtWkp2kKmNns^P=en)_6D`*@KYKY+J3lB!zR?$riYdlMw9?zIk2SANaQ2GjT7eW1fk?&$_%>U1SXP0zb@a`)d7)y>0%t zZhOLchy7#P>JJ?|R#~P`Dy4A#Z=cn_>+hBGTEO0UOfO6JX40r3hvcwXjPKNF`P(F3 zRrUHZ3H}oHlO5{n;Nb83R0{@{HEQNG-akU|0leeVZ9lFu{D05f)p-$b)UNY@>gTQE zeb;4IBA<;PqX+fnErWaw{FBAb2)?ttz=i0r4dJCemiuWtd4|2|#_lNIVVN)=Q2LPB z3oEBm^qU30_e;Io+sUHUBR&jMZ&DX@%b z^WPR;6OeD4eHA$`m5ZZfEedK)_};kw-pjkKEEnMK{MwK|uZivl?=ab@z6HpQ{m&Sv z-^ldVigu`4W8mxdmAG3sb}tF^Y>~g)Qsww*8|YPG?iQp7?C&yuwh=?je>-@bV5$`; zTPwNg1F8q{A-G0r=dER3p6kSVh+##xqs>=Cw?^zgGKuhW*f%1To3~)ad~l7WI9@xc zRLeEq&AJfY)Jl4y7gUj!EU*gH@HJu#kvfhXNE%% zM|ZK3CGFEKalT)zL~Req+9b3bYliqXJtySbmu7Ge1>Fs zKd%eZ3JB4NGUlbKLOuZef*6=zCVKSy+pHMA4SZ;l^$@}*>qNEMIG6`{N!5aVHo#AO zO9%`?`giq|AiHXuVwF|S1$AricLs%4TOP3Q8VWz1Rzl}JEPiuUSEu!ny7dOWkk5g> zAdbkkjA!^(w&bAa<7!uQ3!V>CHL%|S@T@NeI7=_DlVKG7cj2c-o4Uw<*9 zSSceF&BuB8ogMCls$?6=Q?^@X*u}&Zx5&MFrt#*wWl53 zu&(t8ysuDUrJMxgKglB}Abx@W33l+Y&Cama3i?6=e=`Jni*?w1@BS{|?-l5J@F7%6 zzP6(Yp^wu4WH|K=r7R5iiqn@U?R--1(OztWMcC)L@F!ifZE0JI zWfI!oi5Vo7Fq-0C6Yg~0=~)p(vTFssOLYEwEA_?rsY;AycbAf^Fz((X#GhbNv^K|P z6g*z3VzUhL-Q*tZ&OvSSX5!7Xg3o}L0Dp^5B(jzneC&+%K^`dw--Q;xdfBJutUI@fnL%Xwok4 zF7f^I4CJGb&o~$E@2EUZyQgO6{Rr?8{8nvxa*VXT*Rg)EC&+&s7_F_1u@>&`pUuSk zNiwwAj};UQOI~RIxV_%rv?EQd4_z1>pUC`%ccR4BxdlrfQQ2ua;aD7pwq z(@>Emk}RRc&deAhMiN7-Az8;7s#_vS(%7c)`JHFF-`}6}!kBqJ&pGFP-shb6iAMf& zj>x2MoRmoq<2(w6_yzjL#3^R>ZpB;wR0W`XiBF5G=aFSrm2;x+Ap4t(@UN%SKdky{ zy8a#bJDB%TX#dHi)pQj#4Z{6?-GKEwzhp8y<-GEec7Z-C-~-^_XD*H(57>?FFFUBQ zfn*@#@D;Y_~V#TsidPJnr%-+G^!8mW50*5_qo z8}peK{XFO|41a&x*fDcoiYG0wc7p$Fd`;uMkD0Hy7}j#UHj7?VY$7OjIKMpa#o>}( zsQ>c$_=eS=(Y!jFtZuK1xyC)F5$3;N*8_gStlq|8P}kV-lZplQkJ+G{`jfdi%y^M9 z#zpxJ)kDn2wm!n*#7d9L2#*O^bPH0`Wb)zq^bwI9boAn6|1o@>AkG-~%VSL+5x?0{+zGfCI|^01vl2V2WE= zYihvXSC3K+N0(%0>Ro$0vZfaFT!z>Z`vTqUU!})s%|Jd5^VFMOa%>-(N(Oq5fj@+O zw5P3z#8Io_=a#FZP=8kqjTbt zgO~9{4*N;KzmPvX;|UxuF;zAs%gm+aTA=tsjnUOA+1=W3=G5>(bbd8~#gI-+wA?Fh zS4RGmN|Uv8VunR0G#HOVel$aY{niZHz}j&R{x#r<;gG<~VL3i)Q%sokc58wiBJRQe@sMM_nDuq z(-YPc4YF`@VtZE^ZeS4uIbsx?X3vp4a(A!>z&v^UN2mFyt z1IAcK@1#;m*RJh{`#Ov1Ns84?-ZyAyukAzi)ww`#O>&L&ng(L=gVt^E_s)5t6;WOL zaPHqVg!{eJBd-;-9LqkFdv`LdK6d$Iec^sWYViifIrRPJEC_Y%+PdkXmk@V6gnaGbOendL#QkNtM_K9RcD)b7H&5)RgFg|B zFA7sqMfqw~mw)q?*gvj)Use-W=eRxp4)mkB!gj`ATj{{x?dQ#f*fLjcksCH#TX_}X zDU94m+pwu^IHR%#gZMN`NEa2kTx=rqgtP83-1kvJ3SGkfLLNQpDpXJ4SZBhw4W=p z=yugDJ+x1F|8&SJZ?L~1s8@+83-=48X^q-4B(*ihAIKnmTSBlUshP`J9^H3{jr;-M zTW#|rgM_%6Ld{gTKNA>j{KJPi(au5qe$X!ieis|M3H{l;q|T3O;7@xAmfY9b4|`%J z55Fb~@$N~`AMHxd>+OvP`vN^ejksd^7`Na8%y+=^?V+$P-=|*sx-I=UyH}|9-%Hs+ z%Uya@R7qI{@$r0*eHB{VtHqgr!o0&4`2W|Wi&%$)?@!w>UkmwuTJx4?rmNHQNNeuF z_x)^mQcOfrdR3L@N{>B4e;%w(FNfuvk2h$vq5Fh+Wm(wQQ_>!4Yoz%ObvHvg^N(~N4x`X&4qf_m+O`N#C`DM}Ka9{ce(C%Iwf1>0u-GpY70Qp#YDHN=K zoc2|viOq}N{ult+SC)D4#f9uXmJ&!r4 zuV6JHYR}48T`A(nLM!2U`%icCqZ>82K+1dgC<-c z+M~-xod|ETSL)e!97{fz`ggQ>SnQSUwTF;~B}OfdX)-h!0e=ni06vl)b&IC8Tj1|M z@so{lB4I~1fL_Hs;)@8UBymMQl}aX8-ETwj-ssw!27fYNGTuVwmyjP$iDz6g&fhuD z*eL_?XojL#Q!FQ^^D4mqTCyMfz%9N-%3NBRfy8TL;@np-Vv%l9F3(jUV4 zQ9=sK=5)@r^Winm-G%d9(dIVF*w&lMS zV-7O;{cxY>2&eMt^urzN1`a8;3+vYrZ>)@rlT41?!l%G|GSRhD;T*#AKb$I-LjF5K za8%bMQ#X}{94Ab}{Tjs_UG#57E@{Qh4EqY>x09TU?PR!dL;M%7Li0d#=Z<8Q4^(^D zhJS~6$_pWmCKeSH7M#^t>5uRWWxu55SpCcncTFz%f55-2peG8858}=wticf9Ye?=Q zZ%sLna02G5)CX7ie)#0`YXJ-RAZ~qG9R0n~DQjZAX7$QM(rtJi2WR*prtiPowLGT0 z>$;U`2AAoxCSA9$ul(ajm-T=bz8#ur+P6W)s`+7QR+i8{OH@oJxBJ!{`PKgS644vP z4WNH8DY#^ObC44vwxS^Veb3FB3H}sS$@pEJT*l%{{hNXyJz4aZp-;OA^t(RF_l*X= zZt_Bw9hb6eug_Bt2lan>pZT?$5`JNbf2hAU=E(b)s^7PIw<~XSE*Tw%wz`8Re@@xqtv7sd_d7S{oA5NcL6(7TJZMj^lDg%TUur*Nyf!#5~q zKbhbkUo%}QyGHo=dTm4ErjHfa>)AUd*sw-_*AF`z28+e6qI#ZD#w3qRUirY9X@!lf z3fQK!VwWN8Lo$B9b!m|h@9XZduc#c*6O!5gytHz_|Ge(4GS9XGO{7n$PZ%-* zdxrZELAT>g{9JbSUu&ViTq7%;nd5%9rY8;N?*VV8;2khb@qTkZ9q=aTeUa3z7xn2~ zOKDwGlW7Xw^%JC*ldPiRi>JNmVfDA{Yj+9t<*50vKOOOtbI*gwy0U$%zBa7&cTs#4 zlHqv|<|V&FhivdM)PI2O6WfTF%pW3}W?LYUp5AiaranP8`iTB-@vZQ@+n!aw$o}|51QwM`_u^a z#y{`hR4njAf6wma=E+@uedx@6kJIoxMltWK7(6}FINW}Za9)Ahjpq*HPRQTu(GhJ; zK=CoZ{a5(HQM^he@Oyy2V_xLss95#)vSA*53;e${-juz@T~&>`uk6+xfNz6>0y8a} zR9{2>3jPT8*=HA8@}lqd_HSJP`;X&b)gg8qj+Uu8ZBB?My(J>EGml0GhkkMdeDf)U zv_T{@Q^~kKQJrjK1ONY&+?L|9wePIX?i^Y*64{pHtH=YC9*S!s2ktuI!#dQ#%0c>o zKAz{-BK^uC>|iAGdgt2&dI=51_mIGR8BKcE8}mD`QwRE2sw!|AYCDJ2eUd(!CIndh zXMf`2Q8vulgti)ZZEkHaF6WYHOsYF@*xRP%e~8gNRNF8#gF-O*y+MK`My^jIe57uK76mjV8K z2&*XW)T5suq`0c?Mf0p|uMJkIF>$7Jy|l0GujHdJG;>D8%9aew+xZDOE`Uyj9Pl@RX+ZAzm8V3 z;L0J3Saz~~gR6jlaku5D@iK&`sv_F`NSZU^fpyY#b$@S3LwxY(n37Z1ozf+C&7%HI z)IU9QiPJcxhw@Fni_sGYrlmrMYI^)P+g_ysQTp_62cvQW+tLYo`=`}5UDce@j_2QfN1{yoH&pm#PdY-j3#$q&`1oN2B zcrb6?^67#Wvaf2KsNCw!N0fTS2+yIO4E$I8md*vY``kZeO3}PlXnqifWcBw-+slO? zJssRgPIkk$(0vN%??UG@!wxPktgVenZ@7F9@C4wU3$*r!(nlpyL()=Ff2&bY5J^+z zZrFjw#`y;r8|GXQ$HuTxlIq--&J?{U%D(bM0c-;=iabRfhFp1${_JyZHn&cUsp ztRP<;4snr?l-|4`onFs)E!1N?A5SlLG;PO?WBpO$OjAdj)fZttXP!w)^g(}bn3rF~ zDtoinT&r`xaQ-tDm&LJPk)Ybvg&}^pI>nMiUfH_!=bjP)nwO~|hF_+COA^~_;t2Sy zg$MeZjH1I9WhV{~``Um%raCus?1?^2KBDc=UqUz#jhT6CapN*sfu2x5ga5a`krbQx zR{X?tGvs3jd2@6>E!`~&?{>hvyb%ASwc$di9R7%N4Y5S|Z%t6+fic^X4Z#;jvTPv! zP}7?^O)uC@thGD$B71>(WfJrH8`jbKH*BbfPr$x&5ov=xJvS%1@W>xh)wACoc2CI>*~`s2Hx9um2|_QM*Tlp99X5;=6)BWE6Px~B zKjZvfH!~-Dy-rlW+c)Ol3i1ClJM|L%(tm>Vo3eG*!H0D;pip{xV(Kw;87%HmzH2^skOuvq7cYv*nGg3f28iS7F@xp?Yy-ymWR$e#6{N3%g1XC?9jggekjbJ`?&$W8o?e3eD`ZLMcTyVPYV0X_giXqG|B$q)#u3<-dB$7Cm7J{KOZ*h~m*O(ICaa`Yk7WbgUjZx19^pee2iT zl!JX)#?dX^Qg;!Q5oDbe8bV&6~0Ck5Z8I>0d=6#@LxLde5A3>aTouc2ZN_6hUkNpG&F><;Uzn0N_z zS`+0jJvL38M_!-KS9*>l){Z>bbq4j*dj;q$N8gtkV*8AdypURMb}tk01B7UwXE_p5 zRdZ`w+Jy7>sdzh{d-}R|*?#qp(RG76ke&b*7#JK} zP*frT^?tCo8an+#-*OF^f6jva10Nm8rCZ!zcJ(Lh%K>`|^=@{=Wj1^nO45V;5BLff zpV5YfF!i{9VZR;J^8^OtV~nuD6jc)sKC=JnAoaXlJ#l5PTPKQz^-5N0fgWAto71{i2OEMGY@&s)iOwCCIq{Jj}P=Ybx z`9~8&JH(dk#23^_qvucA7g=G=BW}2;<^5a&|%D~!_9Xc10e(%ITN^>^;a9{N+;&is?s7SS)ue3P> z{m-y}f~3{0pcZ}}`QvH9ZCPnty_J~q+b6u+d)>^P+8tp(And1nvUDlJccH1@Hi&JP zUeOSP|1LaF&)&`k*k~g~i2M?a?2{1_1wxUTLD;*7LD-wM(vm z`gT>k$R{?kN7z4tX@rmDu5jfph^6zQhr81*4e^crW*4`^{tuY9wp3GLFm8rzPG@sr z{~*!TUP1Wzw;StSAKB|;>(qMfmH_&B-F5Kz2e6F-!@0RSw>3t^DaOqeE{$g8mv}Pf zF~0F68TvnUP8vIP1|@<1SoObiwa||O`^O62c#e$h5m8l{f%vzhkNwh{W6LZ;dHX>BEZ`mZ`-;t^=A5{iR>eEQdQl|)VW?49Zd{H)UYO5uA{r=y;$WEr`Edzx zW&ibKx6--)>O%gpgTr>F1pK>klV({@%j6+7A5&+$F4VtRgO6!HC*=Qbm*Tf?o}Tr& zi=HoOraC0qegwmClI@>xW)a4fMly`i~^(COOg9E;dBBb}7%KeRRr(`zx~kBZuIQ z`XRAoJ-Z)v@-@vlR2k4GDE?4NgZZABi$o4Z_xC=VrU3;p>8m{yFXFD&7?wKDQ?m>D z4Typx#)y9jG55`pX@0wtwx(Rlvp15S>#Gm?*;AkznF9S$2WO33nT#)^O1s~iZHa>W z+QKf_C`~_imN4xi&qDp0P*2YpV;|8ow|O;$eoobOB*@tjUY&k!k%0V_z5$bg!wtWq z`qubCyqe(;(=)F|CW&7y@v(z_%9v60rwPUv2b&>Z&ir}RKm!ME{&B!Rk2T31#=sx9 zP*+%n$2I0_+nqQ#EyUkJVwO(*>$OEBBaZ=I{FB$c|45)cLu|R?nl-ci*{s}>5eACa zS@bvEz^B4|Wki-;t+mMQ?`Pvtz8#Y1$haOKruXAys6Twa8T|-r1|zG4Q2Vc%!ji&B z+xyK@z`xB7_#M#u@ut{6#Cay$8}76h;@imf$pHB+(a9zZz)SFcL|J;%vp*#p zPIy7R0_J{l_PuXMZG)=}eY9z5apE(NpwzHH%+!RIleu3Y} zl~=>$TP>{0>XJ%?@lr03T;SBl6!d^TNKP@*ubH9bEbx`;JYv&+AibZB8%0F&vP)3> zB&^}alnlnJywWTonkvZ(Ata1F^Pf*=Zvv<{q_%+jkH^Vq@nQIit^46M z)Y}v+$;8Q<4k}TF#-*Hnr@$gtb_}2ys&dZCasmS8JAdF}uJRAx7Qf>p0 z6}Icfp?=T^`}?%GT76QfyB4RS_c!c*Lt;qxDI8tmUM4*M3whZGcACAkQ=f?jeft(H zCA;&mT|VLQaju-gifNxUi(wHWc3hHrVE6#!?=vAi)|+Eh+%%dwt0RPZ6}0dXUB-_a zZBa_IyOus=F|+EX(YIG|pSG@JF zg^9Te9K(SiXRNx2B>?;4@y+)%kq=ug6G{FOTBvh!x2sb`D8%C@%2Sv$@TDyRmr>xS zO;7NHddndHX`xiaf8p~b1N=6Nos!;M zm>IpfObf%pZe(;A5r5Zr)|bckpn3;CEdodPA4jtC5b4rhw^e+1#2%wOfrpUmLJ^XY(_=pzH zsp4>9RmI5iYx~_C0(F2dsA_U&;84FFRzYv?bnJHV*D2LR_^KMG+XMhUDE_OY)yRYM z`&Y&2J?BDyU3+D@j>Xc9vg~3{8Fc<+oEz}Nj0ViPUPiY_fi%+pJQ00<{d_zvJ3c9C zujx6FggE2>`0G1H_rB^Xq5gO*PrcBRA}imvY90Lk!D0Q=Zhl@``=6?=sWU`;kWma1 zH)@}(t)UWZkNC}+Xd+g*MfuiW??qJ6d4_s3nc2$sYQLK?A1xV4e?{Bk4Sf0xWl7T% zMaXxi{P^;s!uqAxIjKts4;${YBQ;MYCvFyfm$CZ9E4PsmXUew;xutySI}u@hK|@q| zgl;ljeQUXwHqtXLkmGhO*rxLS$hh9h1poe6U|$mdhhjp+{66&gs*|mGR*N#P1!;8q zqxu!}qbTFt%?+7r+2aUL8-{`&%oZ`_d3F=XCl`w~u@Pe?foTWxJ`V z>yHZWUi=H{kv#nvEkjn<+YFYyI($Fq=Z|nC^Ba`!a;*bp!2Y@ko>p!B*uOva%5E2h z*CT{emQ&=4E0KPOR!;t|pQ3r;Zx3bg)1F|@z3kNO-kj5-2faaG`}``uM@j9UV+Fi< zgGZfHs_^q3jD@$cprtLYV|utke$qoB+F}^*Y*k9YHOQw3%{^GZ8(x;>Kpnkl3-&xg zc&wqaiT&}>%~TJu^?B0XN@=zN_UF34u-Uma;9sGikQP&Z&dj6CnCnt^XL|wCM-WXp z{wt-0Lohi(Oo#a-YTX5%QzJQ84Ed*!eUV>5XAttw z)ybC4-0}lzQ3no;B7QE^%bBaEceq`uawjqB*PU&kpN0Dd_O}UGBjN9Zo}L@#N$gTP zn=j<^FGx691>Ak|^(npr`4fUV^fS-UKth(;2Y6=|+iAD0=^sm-#FN!&;O~0aqec#n zdX8n=RK13fz0XxxYRNxzQ1u*`KIpmR7@_Z`(@(+NCF2m+-3kw&KEm!UCUUmFdFsdd z7w{SAFL?U-9g7`QPs~udisG9f*^yVdR8sHgfgn7J@6|*tX~u>X%28|i(9{X`nx9=b zvU%?^ujAvvHtld8dBrh0?#VCPs4JDxeXa72!!hHyQLlrDz7Rj5ANjHfZeeY5@}KgP zNH2__C)Rn1>MTEftOoEWdzwARWE2)oFZa7OZHnp#M!P=D&ApfRI^)r`|NAY@G{$Zk zaoO##yR$7d7~Nm~zT$={yL+lc%grAVKUkB-br9(bPg8Xoa4~Z z{5$k7XynKlN8wH=bdf9^e73vTeTIF?{Mr^G2kns61Hm*lgm!Mw)_SeBYg-=Xo zX3FLA8n-gUAB5%=XXeHqI_RI&NksfQ^v}?Bx+|aW*KvgTrxxB^#2egZ7s4KuBhRm+ z{tv8T+OI=rP>%(ANj4C_=L||Mn6Z*&Cu_SeApcc$egl;u;?iw(%U=ra3x`^Ov-|O* zL9a}0ECb<9{>ftQ&{9c!SUW4w2H{D_CsnM5#E#}rd{O^I=mjP_HrDh5=Bp3!Xj21Q z0`?OJn3q4Eo&da3GQti9K`t7xoayGq*GBy$hFax%2d%Vv>eo~q>dh|8FB%c{(`s-= zX8}+5ux}IX1MEXoPo4CB0r3N)I>~LW=Qdn#E{v-M|37WiVEt@^)Qs$F;;Iu5FhU;z z=hol)V0{Mfy%O+`P|u2qi`~Ia5>I%4y8kxk^44@M?*nLF_)E9R8Pxv}dZC$Hw0y}J z`wY<+@IKVjy#qR$>kn543x>Wk4Cb&Ki^8)^l?Y`Sx~maBqvl!L*>fY-o8Kz)f_g#= z`?F+`jbDG%_3I-iQNK!czI9+~wlwRx3gug!J4q4EJBqgOVq!KQQiJ>%^puNfIT2Z23gL$>Q8wT&p#CLY65etz-dM7X+$guRcP(E?#v_HZ(hA!j^{Fa3FYJ9bo=c z+pPts=b(Q|WY+x;u)iK^jMZ_oMI>dPwN^ine`k9)|NC!~VrksxzON{sWUJ#eo0`&+ zuKzXg5awOk>AvS6N8`ZRoQXPp(bz<6RzT1GQyxFC3eJ4?ij`~H| zk@;P3yYHkL^@^^0)R0*F8-LAQY_lH=1wGRV(34{v$m*39^tq2C{u_%lHh3&;^1w$m z6yh7y4}!8yG-*TX673_^J*t5_j*wrm3w*tKQcUHL0|47kGDorm`~!E z4GUzm^`*JlNWZ;>kYBtYQ+Ruc&sC2o@Xr&~G2!&lT}Q;Pe=|x1{ehtxX8|LdAu7hJ zK=q6!UX_avDd)lJFDd$CwA5 zZr7o>m`QvziT*!7dPO5`yO`oEr)Cb-@2Tk#3^u=n4|qW$>_+U?TH=_H&yk>K|BK?o zT*|$qc$~Y2l|_37@UwmFlxhEmR|gLy66(Exub!?B%r7j`%PUv4{*Lg~jM1qNBztCx zg0e{29Ewjy!FacnhUk>neGCf9Hw9jTTHO4YS9i2{q40h>XKkSy4_#;WtbUE`k?JjL zAt0|w^J!QL`2QeJUt@HjoNlvn=iJ7v=)MsO94(ugB+i^=1w;M-{6gTgT`~Tkbb{+t z0K{L6x*tb6h@EVirpwP~)_#C`&RAej0CaPRk{3o~(l}nltIiG1%wH;IK9Y(0?y+b6 zkI#GO=cv>m=WPK!J=tX+x8a2R7m-8NO<-~UtJn7Arv$_NH0TXM=%+9?9+u(J+@W8H z!65{1RAF?8ZI90xs9pWFlRUV>1Ny~s{xUL$eMshZTc`p zE+N_evyMj3NF?MJo(#zM7v8_`(6v!mm)3tB?@KrYuii%w+SRQV%CD(@Z?^^hN5y`p zh=WK&t!pBxkA*t(nl)wDeQ;G#i5Avd(|8@yD#_a=Rt^^;|4EH_`G?Hcr!l|z1af1Cox_1YK(C<-**8B#L-t0}(YfF? z8qsKei>+W;Ir!tae4n#Y7pezW1zDQqXf~}Z4=*|j_W#-F)8CiYU&?Kj3~GgbJ||uX z^ef}C*4&-6OOrzU{G3aLofA$jdk!0C!RO%_7A(-iM3yaTlV*MbFYuGJQ-#{;^dB>~{@QV(P!hHz&UPvDc_&`>B6G=tt z=}kQ|5fmRoy=5T^juCE(hg6{R z3N0wkwvt2J(L6Z`eQHG5Bb_ z=+nlLz2QW+hld=1e>42xR|)(;2kfsZ-n;2qrjzx_7O1yEzHDyY%r&+Y5pg^8!*e;= z)YV~y1Mp+jPy6M>0gsQcyQT593#()&|741 ze4{WPS=+blLhoM_w%TYC!!+S_hUQMPT>V}gCU>iP+#8b^FOzT{r%%~ zJhG2!FPe_r^`08vyURro{}2+{Sd?;ryi8SEn_alr-N56AUhbE%^MoeyXJ-=?YoR`< z?R-nigxyFs6~}mxZ`&KBnA-pzry7`=h_nqU6Pclv zt%!g1&Z3Y>2R$T}On~3jY+$FXH!ye{HQePZ;G^dw@W|fC(2Qk%l`%LbtnYIoa3WC$ zQGBQadq;W;J%gQZv>r}>lt%Nn6q?nDjhp$_WZWxX#CKB*#fr|4CdB=093;*jaNE^m zuK-a>@MOZzZSm|#dVOxOGt#%ds7&f`w*mBS_~P5Y4E8)vg?}lC)#!#016* zwar@AiU+)M73i-nRfGA33F`hBlllS04DS1N81#3TWoysf0X|V6Lf;sS^e;!Wzcd1$ zu$xv#)kXGNjoUzD%B+zoqdElv-k4ykiQy_Q{VCI>EZ>ImXKFz_tGx3gY|ky#NBIvs zO^spaf8EX$8@Vs+$K6>RwYi-f%U!kZ2k;luA>Mgo^N+=q-rmjt`-J_SJ8@Z^+%-~V zM`^wA^WiE3{msgm&+#9{=oW~7uQ?@I-Zkj{RI1?hVXa&kZSDMR>c~ zS$Aig^zwH@c%uIjookQJ*qzjb{LGj(_|+TDHv=BHf-4(7B&O5~`A5q%#j#6s;voYLH#XaRnzu*J0dKEk`UPyOwWc;2w)@Ix10mAr5+~!4+-G9J; z`UBuW%^re6<5YlcuMfti^jja>nK+c<1@k?N>Q-(diL}PU{YYzO=@qT7|5J-=kKzfI zU)+h|zQ`{)U;Fd+?uhM!+ea+FK|O9wzRf_?Xu1_m%MfcJieo~N<&^fKN36PJrb%_TO`adc_8 zFBeUdKS4eQ_$wE0)vY(k5|Oc(PDnEU53kr%JuGPSu^AT(VaqR%@cE0CS%IN^bpNUQ z8!b);wz0SsSSYgB&=97{C|P3tnVZQj!h9^Hkz}Q?M>0(OR~z81QOrEQ>}@AIPSF1( z4)SM=O5>4AV?6GkFqJMv`I2Ge8_jT>JA3@F!QZomiB?JY-ZL;ydC|I!1MwW@LDJ(Q zvqhfYtS^D%^_LSH%RpO78nFrp1zSt{Tq}Uku ziuW3mgtPzahkexRP5#>);{PnB&`3YG(w$i`m5{Q#Jy{FvDJdH<@kHZ>RUwa( zmNlG)ey=Ck_wYIANQj%rx+Wupgwor>CrebyF+&G~^c;R#;Eu(AN{aMFAgo z06xxtqj@E)C$^mP=xBl+`M>%6L%XJPG=jD}%|>_6L_-C?os9(@6@l|BoyIitc>_9P zA89fBv{5~;0N*)S-oC!AZIuc1+qBeJG7H$35_Lq*c4eUcj4B*wyPjUu)>Jhj=eF$w|pvPt^=MUE2%z^%e@*9X^c(mK>%jpl0z-U9hB+R8t;?Eb`rSk;f8F3G)H3xyo#jqVH;l%hVtq zjAAa%BIc&2V{3XzV6P>;SR~Z*8PT1IC;F!g*E08KSkvrL|Jd@R>D8@EP(0kYBM{4p zoA!3^JGAMn8GUQY4ulVmwNlQa_=QD9i!Dy>!I6Dh(Y*NtVP~T~t?0I4U-3BP1FvQb z4M*Q+jjeoK|5xpExPPPU2(j3r!^cy3Y4SYiSDL8Nzbq=b0OA;N)8XjzhH{`Mz}-H= zZoUiR<^Gu}t~H0GS+Zk&u9-3LN2BaO(EEdVRbE0IS9t#UwS`N>?EbZ7eV0rq{h$5W zydt^|ut&6W4k*btoSsDX9lqdzo^R-(z|ba{lf6=Ebt?hC!n_cyuj*=FJLw!p4tsg} zEJw0;W(e%RrF9UG;(N`hM$!~tGI=ZS6d;TSN}qGS|Rh4{jY-^;`8-`9RFp+_H4R% z1$PYHkD5oNhjnvC%DwMRM??M(`+{-AfB^22(hnryN1CQXW1B5Ea8?e6uYE~`cr^k0 z8M=OL7?WYGk_v+R$6?33Fbi14%_nv8a2xS-!>8o=uM;B6UyUGoRUZUk>N>W z-JQDm?)}nbrO02@c*V7Mj+j-Bh$TrOK86sPKT5kM{zuO0rP-;4WnlJSWNeB!0#j2g$Nl&quf3Z?4KQAFC3$a1&lw-ktW6w^HnCZ_mwN}Zy*Ce!Y{?pStvP`+i9qhg3r{T4Yxb`oL{tzKl z`k{KZlVbtsr()ID8U?$<zi$@2<5x;~v(>4bLnod_|lv~+chu0PPMdeSHX8RrQ zUl7`FO3;NTR|ogqV*gOK0sApD;98PK&WX3_wHOuZ&%npfGhN)B@6IuXzU@=;N?AX8 z65vlb`~7+mksgPW<_-=ou0VbCG09IC>=V9^LE!0U8U;%#4!2~X`8a_ie(6$tlul`& zU})FUTMy{PQV=gDFV>F<4h!{9P8A=S_Q0ofy8LqfX!l2=5s${Y^y z303-j_7&yBFC{7}OX2;Xo*3juta#g#!j{p<-2D;sT~q1f3`S@^S#(0VQz>cs6{PG$ ziWAz1hVp%EpR}HvtF0_o>>%XV;IF-GdE}za*KDKH?g;yHX>*T@vlE8mhhN4C&oi>? zhtCy%31?I0h{bYB+20PENT5IaM3z3Z1^W3-UE5wl|FxjTm8Gt1^3HxG)W5K$9KACh z1P^&_G;it+`xLex+3&OB(5KtWLyXC#pP_#qtL8t_TZ-a2b!DS&Nz?n3__npYT}#s* zfBjwv{N&7~W1|<0k)8%Y?@bdU0z~*rBWh7TAQ)jDn0LMJAa(D%gwTJE4!CTO2}=~Y zqw_;}o*XT^wW;+{H@{x!fcra2@c71|G45wesW=Ux`$(<0WXr%c4-?P6gZm14IOns< z;}u%_s_!n>STz#Q&GEk7iS&SB$;U;|j~Wh*@hXfGX_wpjc28;UMS<^2ueU!XR|9y=noR>ed6|Rr<;NDI{0BQPTgPXvz908A9p>vuyg5s* z-Lssw;bW?~vb!1+4esN=Kz$#hE^1(W5zA4e-+vrk!=AZvaDH||{1MM%ONQTmKFkYW zQhl0J3HTTI{>w`=7^ZqD#b^0ozY~O0*3CZTJGo5Prj>|~pCjfa9_}1jGmtSzMDr2E-_i4gedrE6_dUy0 zrS^<~e$$5X(?d*bJ*gqf0+SM=YpQMvI{Ff}Zg8dc2FmH&}y!7kW zLH#L4py$;7HXl^H?tB%?*mZABUvkY5Cc13ni~9tGS3~tTaCBIfl7pH%%``_4IM} zc_SbCyS7cI7oc7V`#XFJdt!EHY;6R6XhGV|x1Hia)ka)-8XiPJ!sg_*LNExcI_9(kWdLt9!y9Vb~R`48p{eHt_R; zR6Muq`elVEVsRY869O8~R?{g%S$@jj8r9#}`RZE5`3v{`L=?meC9DjbY<4E0c@wJ3 z%EhR^oW0+UL~JH!s`j(61m^$d1&hrLjr>%A?}dGgc=~~X+yK{f@s27dgiuI*nP~Yi`Z0NJ9{fdn9mS`tjl$pKdJ4|(K#io z_ve9rMCZ9Q!Qq3Z-_Ma@-hXOfsd(hQ!Jx4~bib*uzI^fCp1|_a;@N}$;jmA7b4X4r z_TtO+AwRaC2?=b>O?msa3)JflqJ9}H1y7OB^tIw2mY*{~eBcW=;On!MK%Y^(74icD z6v$a+M}8dJIlMT*&-_39pIK!;(XOG5<}c6hp#)Q|$c?r}t^j-Jt#J{IjpbCOr6*Rk zf#Z#9+hVxd&i>p#@Ne>mclx<0M`;Io@c2@Ppb58{S6Hw*>p> zVJC0YB?~s>C9h%f;PXQL3C|T=U!u{bIv3&TK-o>^uGB$#{ipbPOMuVhP+feU$;cEk zdhYs`Abds0cMO=?zP5=~JR^_#OW0n`rG*x$%W{8?`L9x?z&;b0hX(w%urpQ+`7=Y0 zAa1c(NBV!N7bp&M^T z@>!^Uk9je(i<6d-9whgiU9z0S?%w9~b8dnnu50>C zxDA>N>D9jXk^NIVx=4A~ml-{{@hcnUYewhA!ZI^!WZ2)+(!a-o9w~KEmY1!^!6QFg z_k@YW-Yng;4xS)}eX*_B4e`B?`U8x5+mrCaEHSX_SZb5Su(al^l;F`v?uT2EqMr51Dqt{q;P;D(8T5HxSoH5i^_tK_ z;q=`{R7-yJAIa(|1bzQ4x7PxnC9s<+(Q5^JVh7VLD~krdO|G?i4eOY1pyIPrCN z8aIJIDj8)L_*!r_$g;fj=0dd*{~1^;M(RFn+!f{IO)agF7XS-MfY$+`nd+TK3O- zWt|m{(uQAe>%cx^TaVL&V=({RjOK|TUrfu|;n?}Lv{tPJ<%7d>d7#&^GWqtIl#h$h z_vJtKB9j?!-`Z{RTL%871&fYouqV@$65CgoBK%yXHoQTLOV)h9+|pE7Kl7j$lDjuZ zv5u_+ezKYOn@>5QaNv)OK^tH~VV@#Niz}1x;4IqrXa;)kr3Rf{KVFn2=87ZxugNQ3 zm>p|l9XQgt3H(uf6gI@c!&hocN=ktIvKP~rr8`h2RL|-!4rp~m^=*l*4{?J<-^I4R zZg_rv_wjfO&?5tVH`P&LeWHf;!q0w#xJKf)^JkHs1R?UNsjVMLndmYG^|I<-_G(Uj zJ?Eyf#ohT)q5lfbE-OFMD?gYn)eLxSjxZ+(>a-&%#})Cb;r@Q1YI=!@#kAwTHr0+~ zK16&MTi=q%;niYffrOqJxo+BC!6^ZR4o}D-W`Bf$8KX-NL#tk|K zQ3AzUiBNA(!IK5HitD&b$4hOx;QYK_G<5{tSGICCoJaLxewv>fgTB#E&fP>Zq3nNr zn4|W|M0{_;fif^A)ITpLQ>wVNOK3mljg1a&>omWM$^V4s*Gov=qf((;Vxo{y$%FX> z=tnDdpgQFeer%-4jyMEbxwfr52mZI(-?n=Tx<4?_qDLONqcvx7gJ}c*zdD3TU%JWE zlmC%^6Y&XbHOn1jLEGKl^sRjej{u*r-BqwC;mqo4h!3zotPAk%k-^$=SADtPco$E1 z@xq=TG?X~JYBn9TP-&W|A0vyzI}G;GLRC<=XmYZW?V@Qdp!mj)VcxHWc|T8S6)VUu zTd1StZQ(B;-k85dsetq3g}OEqJ8&UGFm~XE^g*PDXSh#Y)IM+};iRPkn!km9 zkbKLI-Hvg*cbm4tJdd%HtzHt$?^gFe!9zX@`|4k9_a3oW+>&>AA0FvXRo`wa$K$nM z1s_NdU#&Ekwyx(U#-4e4|JU5aE7T8GP4lauTc}M*q?TLaltz8BM!leEX_5Sz@cWy> z>M!cOTp`5k>@Ig*X(8W&_ZIRk;ca=qXM#STcbwVrw!u1YE$Y$l2a4ygk}H}11NjHj zjgOlmzUTjldh>Xw_xF7qAz9LKN{bc=WvoZCBn{dOhcL#Pq!3Dl5YkAIB}%MO60^={@sEm?wBMsRL zu8AeJ_XwpUX^1W9FYs6Q%|*rK&0{QXe_Aa2mDpNZHCci1k#fCx13|uNP(<{g9^15d z0zUxpJ;x|FP`yF~_hhI3d?>_Y-LN0UnMc;Sq*V_DOE`G%JWAA8=T0rO?q~FE0{+7| z9{8|PPFtEZpJdMn>R$=&EJ;QABvuZcQ^**?#_1K8MTCW(T+ZApCBV!4sf5P5y^WsI z?PozRa8HJM$B9uM|3bx1l9mqAThGM7ypcb)FxRU;K>mhr$zS;TIqF(2qa5r# z{yRjDq@QL;&-jYovRbP>d6Tt(@wQC0>ZOIe_%B`@*ulIZl@I=&9p%dLKk;ng@X`nJ ztPbM4C200PwrtC*>$HrAc?mCywQ`^zVIeAm(jSHN8)>Clee#8ycVGVZ*BPY$w2%;p zXHmXQt2qUU+4i9Mh7EGHWiM8HH@J)&Zp>?_JX@9o`uCS6+3ek@{zP*r`LkYED2qHM z?*;a2oV?wVv}Kn@A)Plv z{yM5TYE)0JpBr{t5#m9>_rgo=Rb!0QX_sbCc2B7OQ9Gm8i`YWT`vp!?4fe1D^OK4B zH(<*vxifm+uwIkcK^%d}To!FH@@ZV4FW{}jBH=@sd+SgBFMe;R(&I3FWVsf!fHI34 z<~g(XiKxGS(eUXU)IX-^pl?Lt>@ycOcx8|96rPJpdoOX>=F{)fgOL3Jz4+ric;|`F?I@sGM5-bRTOTDzzJ zE@y-e_$lodzKqJ)xkA~>GI8Y+I?r^cqB0FKQD*pE>S*+`j8yZ5PZ)R|@@1xL4a(mW zFpu=)-m%IJ^0y{Yz0%mytiJW{PwVshpgw;Z{NG6>BFo7wZ}?v7D8d72E-Jo#^;h!) zp+6VdBP_>S?Vi@?M(WGO5{S3vd9UOw&7Yjr(RXQ@VT+P#kN19tcy6xMM6bgtUd5ex zqK_qr$K$4{Zaz8L`CH4qA9qiQEWO*7A04k8Qs>1yXtl&!cU@t_Z?{+FhB zSGHhFoAxrc; zO%0*Vt4li8{eEANg3d4g3(Klnd|giR>k9%rjUQTNNy@am-FCU^kuAi-_{LCzl~sYR zSb{Pa@cwSBq{^P-@#!$u7y$DjL-x5dRf!zXdkS(f7Ds$PdIYILv|*hrXY~l+F`ze} z?NYCoGO#0=D=W~?GuoZ3IKx`4?5h!GBZ&W9SU5-4CSAg<(1XbS@o_R$iy~v9ie8b# zN6-)A#Cq`%>64^(glw2#$1n=i*FOfn&!tYXhrGq6fIaVf#UDaHVC&G2Tnd~=HkMm{ zx2UMOqUCw#H_!_r?ll?}#IHqL8NL>~PrW%-d+RNK9=jxS;KL-`AD9;x75u%3L}kTS zhY0dbe@-yfHcL(s_>iD?N}Qi^Yc1!HsV@hdR^DMRFNyFTnRrkPRr)_AalqhV>3DiEkOo*)){EALph1F=9a0A2{h8bZ@Oc zhVcp=t38rb1^y!E`|iz~OSj~?)hfbzPvXO1{!EO8x$ac0D8d72B}K~hZ!`7_ZSPNa zUUjD}yN~h>_1Dk7WDlc!vV|cgGdSiG&3DNWh4X)d^Xg_eS3_O+{D)h&z@AK*tQU#C zw#7M%#F|z4)t+yicgDpX=HKyfYUKt!q3C{4aqY7&^1m@Dp|)uMO9z-%^<$2im!xvb zltav1h_4>1iZ9w%DVp$TnO@zN=aTL(kw}kmj-z8Bz zWz=A4RdKm}J$qk!G<<%aD=YLW;B}7=&fKo5fq2B&=?BqmO-9*%`>POd5jiwVm5RCs zE~WZwy}Z`Epmr}Hi`qNL(= z0Pm*lF7G|hTBhAqzFJD)f5PG_^&QskS1D-2$+^4KZZV?p>2nXs3XNoZTFC$LBiF@t z{WD50lRtUDcV#ATX67P(bo zihvxLZ?+JbqHYCz5>o9kyOV%#@LEln53-ScBIk-J4qCfw2lUoRf`4X{b5$7jQ3k=d zqc1(t{lsTc$~vOfC-qXg8*Ed-o^3$$ik9f#!IC0-%$JDRym|A4%=r5|oL}uJ)_2aj zL;?Q_<{kEV{MsMVaJ+vz^cQew)Ex|2_H^9i4JirgNKe-un{KpCTpGu4go?Via*Dq9 z9kmUiznDY<{c_Y_JnOngZhS|s`;HXglWdbyQce~4cU~gHm#F><7wdIi74n54%wpyr z<=7F~9w`$o_UbDy+-|+R$C=}|Xm&)dMEoO~EA`n4PvNPC;?8F9hZA(wUPk8}P&+Fs z7EnEK;nr*?PG+lEJ3-6LHW1F2KUGho%HlXSvd&L{7x^b^kXnS!<$Y|3XM^646Y1kk zI8QCx=4g7rPaVShQ?ERIDxrO#_;?VyKarswl(-aWC1&-hGl*}@^Lw8tBDCtA@x}_T z8qj|tJ62hIw^@0Xx9y5p+6}w;|LGI!4B3q9s_ekhI*-`-f1l(n(Yo{g-CAfh2J_o# z16_^|4jhca0(_C%k~u($8IW54Z5fS#@D?(K^lV?U?WUj%G|vX=#c>A-lf4-Y4SaVg z@NaB9Db&9Cc4%W&XtkLj-wE?4Fzv0(gX&Y2;qwa^dskwL%}GLYqqX2ZG?l)TN-Ddm z^~Awk4EShROOs}&U4=xAvUkn$ct%lorMj9F@b52LPe$xU^%wGPYT?zRg`0BnUb>-p zIVvIcsh+<1?UvyC)@Ax6I}$S!@acu7H}7lCn-;7ILwL+FzY_~L&%TsK_z#}4@Z!bz zKFRv~|A?99>u!fxwq!+T`!WjOnS}NyGQ9j3@yfbeHb2gvLG@jHd5MNolv&US-Q5Q4 z=P=#>`@U^zo!QgEPqzyAPQxXClbpsSl7H#9U) zbT-(TD?`8K6h55#yFw(rxFn#EPT-s|(P&Pv$diO5os=%UsQsr9cflp%dy zEK3q+Kbn)uvD2)Fc)IzE4$@x&{iIXwaQ>j5&?~4uGk=wS&zYe!Y2W|)k5381u}--0 zY}4THLkH}G?1i$t^0?!I{c02`!|~V4&fYCpK=E6+>n>d3?7q!y1pOeyN2I$_%Y{TT zuI|5l@hs$DBlL)HD;b%yv<1`W)^Ps!k@`PCJd4rx@v1e@{lL~IU++JECG@OD{SA2E zdA_P@R)2^1bb3^0Lt3rfe|#xP-2^w2e3-wN{bd~bE_UkY94lzcvGoW0znh?5|JFiR zXv%r#gmTn@QN7%Lgck;NI%*<)qZY}Muai7eMS|S@C0mHMTio~+6)8f60dE(F(eINZ zsflH?LNx(&zebcVk&VCOq9=q4efd+F0(*5zMx>~;ye58=V|cooK1X4HJpJ$c3dOs4m>>AIbdeD>|ZTEC4(qp|6 ztvK1UuH2Qj7vee4`?Y&J;PCzI;4OI}h<}FgCHh3Q&O0CWZ~1V%tS9l=37w|vTtp`czc0VKjBO3^OXqxK_{T33Jr;+w+}h~C@9U=5za-U*{LtAvA`1TP)hC+E zT_MFE17kfINf1xN{1zW*C+HiO7ZRBsNxS)f{NHr(8}Dc|uutuDmyi?tu56=mglSaB z|K?4?%uVggKCfwTR$zj^oR7R!b?@uq@e1A3+{)sP;w8XpGFKhGO1x+}bmDgC|uI^aK`pA$l{(u~X0G5fQ-0IgqSIrGwB z2jwg+zfMsH@v9>qw7=#=CbT40^h{gVE&vm}{&Y>6PY?srV-sjf;fe8MOcC8mrf&FO5JK3LSjb3T54R8bd ziuitx#MG3rqq3Z0SijEs@bxXtk8G#7giX(dA)cG3vt#Rm?$#B?di}(q_gnBzd8k(@ zw^=1Y?k|Y1xU_n$eO(Q%S(T3nW|O?dZI z)X%B5hO>hFD}s8}vA%t`pVp_7g6B!3N{)4oHU#{+dpcXs?~u!y&qUV0FrT=!5{Kdk z%&}^lnffg`p;Np@gg-ZxRM}8hzqt_agz$3U7yDCB_}Rto%^Av2+v~g|ZmcXH_!zZh z-aKYy3-M-TX>W&R=O(o=P5|(`LEjE%<`*3BBIS<8yk$0n(sUO$4EWFz*^2NL?hoX@ zN#QPTEcq@bcZ)BoAJ8nRN(BBIrPvLP!F!e0U3#Z)eW`n{6;o(%ZxG~n-c@gd@5&P% zeDwL#HWlVyyKXH@5ph(krMn09qyqkxC$@ySkHl%5c(`doG=$(}N6J9`3gOiniEy4L z=((50am~^uJ`K}a$lu}XOJlC4j2%q>H+5zPJpuagPb!j}M}}BqKeRSYmKUhi@~h`A%YfdYrmsc9{=B}=!F>Lu z)_@+qKH`JoBerU{P~={D@BOF%{CgLqH|U13s`<`_fTz>g{2V3=!cVEZM{5nhUJv12 z#bUk`btL2l8sGe1zh6<2gVP~ePv@TRg8nHBYEcDUUQx_c7Zc>OgP~Ro%>rGOy!d?6 zlgT-=;Ja)!jn7NgcBSr>Y`R12K)=)$A!M>w00jmPis$?K>g6h2SNvr zTr@BB+UpVu{%eYUu$S03vyEV52Ku*f9u{t$xtHYdtm1L!0Q`tX97z#H_6k$&nBhnLQ4y$w=1FyaHlE*b;oWD) z@SS-ouBf!+c%IGb1USFY@3C?h>*)QhxySL`9U~t@~8N0DckZ_4j@ach>WUe0MGA zL-AYw32)R;QZB!72Ui3&6CveefVvt%c8lMLxwv3I^0pEU8np@3O$!j;(l|V$mP%2Ps^gLcwvqz< zOZ_)HcT(QwQ~y{|kMd2ZKa(N@Kguq>1Lzk~F6a?|Hzh?BTM~Qasa6A+BTZAV7a3K97Nb5u4 z3WyIrN4lv^ynDA?bHaR28RFA~yHXQh>pRvc=ZvS@2E+Y`>>8eaYGItY-Onoe`S~F8 zQ;xbU(A6g{a0J+}N}x@K1(JPA$Cm6pzRY7unRma{DXy^3UGn@wo(> z@$mv<$S)^AA7Cem<)XRhWj%n-e^g>fu-(I}3N7V#8e!f$v*gUWdhmMT{>+^wDF0lj zrxa^-S(+7}=?#8K$S5?=L=@I?;!6TB<`3odzMLj94$Kj3|U&q%!zQ}lGx z+iVjQ-*FZieoWUETBIuqnP7-N7v=Z8b;)p*npz*X;lj6cn!Hwm70lNS=Zt&pLjJ>@ zDZd)kmo8n>6w$#3KQ+~k# zlNabUP>)z>{9;kg7+p&hjxhy(3)IUf6b*OJI|Dh^oVHv&0so=eV_teuB~xpdU##mCEB{aNfGf*I=60phq?5SOG`!%PZ3Hi2#%FsTkp9 z=ie9cyb$0Mhh@gcXxY)5Z`upyRWI!4vQ}2CR-gXI)e^0@5$F>V7$31SmaD}8&l&_g zc!vE}^4&~n0Hy=;!tnmWiB*(j&7t?GpC9@Q?#tWG21PeKh#JpC_K0RkmDv6F?fc$sdJs)vzql>CsZ+Gwo27=@@ARYl1O5KQ zKIYK#8=>g@ZD-J%Ca>uA7qyh#u?<4}TYLnyP^nr(3EMR2E6_V|p-wbcNj_cL>2e0* zb2fIrsB8!0u&mJ6MGWTUO_@9rUJ@2MvLd%|eQP@}om5ce3jG88s5gyeiKZtyCl|qf zpZoo)vLo!G3i#_OjP!-~1?uYoDyMj;9%&NhcvplhyJdPs(+yvVo6Tjqlth>ixisnb zP?9Z?Oxt2R)VzTGUiu&`F9hddm>#RTP19stFMT%kG>Q-SjvY?gxT^|vBWF0!pEyM` zda_1L)VW5k@8#nE;$7iSShzIISLY3vDF^NyB(C*Zn7{a2c=CG<^ec7K4a++?{1s7I zBl>EFzfx)`e!fy;u-^pMPsksjAGE~Lx6|rH>*WuoK`8&C^;eZoEtPJQ^A0i>^xGLI zm3Le$r!UQFk3&4gjhei?^{&v-HmZilpoC}+QTg=t$L|-Gq5_z`yAgiFOK>N3D%Jgi zt3LK8#LoATxG5lI=G`32gWyN}MoePLszAa6xt zNIZafOTP|h*ngglLP{iy$U*jcXuou`G^sw5mGdZlN&;tFJ6 zfS<%H&J?-od*!Ffg^9G@t<{fK&+@UoPZ<`jV1FlgE(`00HtXEmxzkJv_z*Df3GnRS zol>l=gkZoAr@~)~h!njWvl!lcSsBIm#{Pbh!KdC6Qrur^0>1_L+9K^N>p#nq-SQT| z{&0;eG&>0oI84iy3sWOw!6eU#}=0{1M@%q(@)cYUp$U?xqO6* zvk^a+eTScR!|XqNr9Jn~j050d4~NNC?pFJrdFHWtYgML$NE{$|?v)lYSI^RB1OWcs zZIZdPh>_c~6-PRbqWH6kAk+KX!Cn1F$$$*#W3s;H3iq=4_>t0}QWEi1+q*oz7B85d35Qib^At5Rt~P(SR(T)U#5MxB&x z$Z)(q;Q)N>IYE5c66r=2N@M=t`_4|KC>Vgfyw9M6Ab9{O4PPEWJdEmx;SrkjIweMK z<^b0p@Ij7AIJI|=<5BHQ`&yK*1Hbk!D{EGL&Ycd0ez1R2Sg5_-g=s>@-@~7NlzQFI zR6H2kI`&!kSlg>q@~^&_eYd0yGCuM!lVRpAErH&4t~KMH=Gy7TrO{lt&r=JDB!^vJ z5080D<(`&5#_{y$`dwU_XHJc`epdi|5cptI;f@vhd)l77n1J&+M2?ivAhAXga?h;@ zTlwjg`-kmEVSe>v^1XBZYoUJhtsU!h0fjavQpSf5hU}ufx3ng2!g-C{!Z`@|jqxw~ zTE`3B*2k&>OEk)_2nX)91NXKPEE<)yEh5xyeElN8|MSrjnBX z$lH99_qn+A+3ZW!g@ml=})vX4$;et_k;a`c~%jVJY3b=H$T*q1_19KC*SJmt1R1WRp66$`nZn)h{sQ~+s3gdx{CTGB+aMg+t6y}s*cV<>Uikyz z)HK2Q&s0r%SovJ*vYhJWo>*m(0U)D=<_>rQl_77DDSBSemtYg zohrh8XrQ}1NRU4^yi_OFiR2U#@_W*1|F54airhxTshRW`bNKP*!IiI5*7teu;C)_u3qa`CS*z{ln ze}N2m-2~pZ+4xv)^mQ963~y4M5uYwHmAccou<9SI z8++yA*vVk*bEMFxPlH}h^|NW`<{uBVO%@rpVE;@`#y#;H@!Bb}eg9STzL?|E&%M`% zwbDm+)S!6AIP;&&LPDn6&xy(b&THIpNBToy9(!2JkRfT;^kMTwtouqd<*Tr&-`l@K z%MJ#RNTgeUKQ1)cFdtRH`+dJZb*Q|Gy`y?NZu4dxq>t^Bl0*^e*?dFJKu|wkfcZg_ z0a#pc``R7>KH{&53nq;!M`urEA%6$`+T;BWpAXNvJrYCvkN0ux3${99%~hRTtNO_> z=)*7fz1ZBl@$uFG1wp^0f2_VcY5Wt8Z@U)OyBjb5GRcACmegD^cyvd$^S(Uw3l5`m z7(b$~vJ~lMEU3ib>PaN6C-(X(;QtuFkA~Gs-=FeY#j&s;94)eK>2R6#NzFS(Q5eJijOU z<;>}MEBO4!mbDQVq8B{UPS+n!2Yhaxw_y^iT6)v^ow4~f_!F?7mg)~4N)`op1pt0& z#pUOSb=Eb!S;?R64}f@litbqD8u(tyt){>^*iiar1EqQb>Z9`)-+w*rFs-2lcZ7nPoIlT{+8Ds zfaYCB<`70I|7PfjZ<$s``Eiuv%PUe8IeP5vHfxAqxW=VbJFKcz@7rYrnJXzcXSzG* zW7Tt4Hu2_p2F}}~TK#*yD-gfJ%;j(ss^`-q%*56N9vB|JG!iC=Z+#YdysxIM?VAqi z3;KV1D}0dm)K+p7`7w;*UX}?$Wto zeGO!vXg(dL6%U<^zbjOEBK>{*tC&JdkIgiuOHv?vNBHB+H4^*z3l}ebB2A96p?-3d zbZ{A*r{-%vK~Egjb6hkjdIkv&!}BKTWuhTfK15aE){JAR^HM(6GaP=C zxG>nyXIz>ewPI_hyy27dyxPJV0zlBTNu~;~j1KV&VoCo&JnoVv7b4+^uT_h4xJ}jmgTlS{fuH| zIn|GVKm2U6r0O*^yt7JqyCVSB>r-TjxR8i=&!g*aeg)J({h;NVBjsLa-5)uGvS^6! ze(|AxQH`;e?{(Yem_2)}?Zl1x>)d}sen)%0-GTbkn_M1@y|y12|G4atGCI$^_|ZC% zcr8y{!Lb!ecN2;Vd~>nxxn=9JA9yZ<-=C)?xIEBl{G=!R4a;Kdc^`0Lt?5Vji+FI< zTd-dc|8{_$_6k#bsvE@Tv`M-@wd~7Ra!y{&iU<^ML46zNN0hCNUcXNk;Y;)Z;a?c$ zeN*n+ImkyLzEkO)o161=spSXIqSqE%H+(J@*tcqmm7lYo^`SN0p}_aX_`}T&8o>AL zD$;V12LF9YKiU;nE-$Oj)wYmNVWlpWVv{9~eG z4CaFn7&F1yHX#Rw`dH`rkS}N{EmR{ubW`>&{p*z3?*}YJ!Tv#f=GwLGilk&g<;Y9n z9Y4=o^__pzzc7#Slo)TvrvKJo!01;L{pOCgxIcWq5Bv*;FAH(9k{XWh8qxaRgYH{c z+;EUzLQ9&*h2_3Fsr%N>K!4x@@EJUrx+p$@dF_@Y{*cy#4NX)yA6#Rcm`Y!Bv3PxZ z4)7s&LVr9?eKdr*?m@5Cci6unvSo>-72`odR#21UG^q>proMtcsN>$A8|n|yeQkO9 zqvCR*y~4~NGVu3wc2w-r7v2JU167*;8sTfk;R(8n4nvy;zWHkP)PQ|-T1okP(0c!h z)MG2AZ`yI7|3Hy2I#wrUzOgl}?*HnwtBl9P7(o)hYAI!jCxOp9zhpLd@d)^%39^ck zI*D~xW~oSf7R^(ky;=#98PwEydeH@W9Sgy^Rqhr5d9#BzrIrszKC92X_Joq;*&!6NC)Pld_ z8kbb+tDlyqhvS_zsOB$EDq^@ zQ|3R3GvH5vf4HkQrFdmcNNzX8!w~Pa>z@14Za&~ct`&6_bun~GgZOYR3UchtP(K2E z(20UmuSnQmt2wlY^tK~OmH{N0QKR1?)i;j%+;=~IGNyHkH@#)-(-VEbZ!x@~$TK~S zK@BGJ`t-V8?hRh{C)EXfpuk6`Eq)bsQnaA{3;tP=F&7N?mqR->!z7a2(!{p;Wfks+ zd}6TRsit10{r&O-@8Q04$xfw`e?A!17rOa@hCaVg5?WtboGAV4OR-;%9^ln4xP@?{ zDHD>FZ3qt}i`$bz`c{1ff=V9XpY!DQ_bdnZ_kSpyeg^m?=mF;?OIe-Tr1C&S_6Fh` zl22(?RPAM#oj%G9M*e|U@6Y-|GAnc`HUps49O;!->yw?X0Vsqg3`kXkJKoe&ZxMpP1uQRxoj0 zXS3Hr7}yhTOZnukwHk|06;zwNkbh~pCPKk6x>|mv%d)_pahl>fn9M|nB2hDmJBaUV zWck3pu~M>-Nf+pAvhfsZ^0s}4lvF-_0DnfCz#mjnCq`}Ev~)wWBQ2=wzxg~XH}t%3 z)SeE|U&IQQ7y9i=GvquBR6_Q5!SxAMMrPidl85hVuCiulj@Ag%Luj5a6A9(+; z6kXfd;+$-u!?Q{w7jm{VK9m^&ew;uLx7tHqTqHi><`mR}!GG}M>rE#k#qzyu zZ`r{6P2vw$_1jP8RF*qM+(iG+iz~fM8f}v<6qffgJ(09w4)o^L6-78j)xRl_uXd9U z`jedI|9F-4wk-M4%DmPhE!^K@fEP=N_I$JjJxRK2$XuQIc+oM7COY^tj#2EGW<{ZV zD$7;@>@U<8Z&Au7-Y5I`W_^l-=O3cq@9mq@J>y1e2wsltJ#Tk%T?k8YJIAt`tvbLB zTsI#A{_J1s$jZ}f`1?sLvntxvs#lWPyBGGO5%}$V-eg61R<46pdHm{-S6)GereD?bfdYH{vdY$P*HT>2s^!ywUjqM9 zjKDwtY$NAiS4&$dxIf;OoH-`5A^CJC?c^hyI1TPdp21yxsOpf`5eX zzw|_kbue4^+mK+M4$tRhCsnWirP*2^AE-}bEkl3p)yMf-HJ)`*n@*;Hp6nIVnYX2f z$M(#53h-?|&0Xgg^Rzk4I2=09##u0r=dDe@XjakT=$5>nCM*8O*VXd$_zCu5n;Zw_}Q3*N`uhC{mCL1~xN zdIIY^g-z^=z^9C@rrIV)f&GMj;}TuH0rm<^xg`zoq~OnkVF_B&RjoJE^R!hb#C?1u z&OkqIaG~hoS!Fn1zvu@$di9(-ci>cJO$GC>uUT4k_7P&P4mT`b&`WJTW`Exw_3tI$ zZTk!1<@DkdJxa&-7MAo?3*^su<>jkW7oxiTz^9^+XYL|63v$7WEC;>+@!5O^>#F?{^^% z0Kc0@lP>f$g7^>k6p1(o{)*}e=I-QZ)=k~L6QFSe@rv-(TvnRH|KUrHI%Y8&7TD6^ zW+Sp|ck=m5Q4yP~L=b)o`o|rE8d6sKVnyccuEtB{$k9I)4k)ChbKtR=EhV*u>&0?bRLI99NCPh^6(e7E8mcpWB0wUKRd+FO$iTsZn`5u=wj&F6#GT z!TgbncP%<6(SAiX*mTv)U)6lwi)*&k1%G?0r1x>}>y#!9w*VAxW98+P-lHC+C01JP z=->DTE%gus+vP0RYIxsnj!9@$VNt_}5)(J3T(r$iCIR<9d()BJUta_3VH{T4c1k^< zUQIHSztVf2bYk9=F2CDm>buO#spZS`Zn++)|6%f_y}PK{LZ==M_aOVya!9TYclU7lZRUzwNRI;x9ov1Y zv9$1j<|*)Z4~H?=z|Kky_wD^L)xW0G7#wWr*&If0@w3S*7AljjenpuwtVH~gG6M4m ztRI)3ph>Z3*(ux@Zk|E&gPa}X+&J@5j|+d}C3QfL8y{O_N%C-ixci%#0>sx7*g<={ znHh#f%Eqq{Pk{f9AXq)yHnQorS;2F#mk%Z}dVok-EAGRG;$jKl>pR$oT?Ky0<1anU zyKQyg{++Zpv$H$8>VSe98O=LydExk#8uN=VElN&@_2$sjJWo-R+cVUbdNfh~zV9&j6`s6D=)H@dMxQ^=E__!j# zPe6ZCI+6mvH$*RO=zlBkh36{2+A83uy2>bp4X!(U{Xl4ygmP5J>HZQc?$ocx+cxX@ z!~aiVY|XpRp1oUfN%3YtnsoDjeBbzN)~QoiTNzcrZwy%Gqa)5eR$zMajRfM8gp^#o z@7mo~ZEFefG@AFU{qOU@T`R^Xwg+mRthxW^67M&?C;r+~*l!GLFHhXT7_^DNd&?qx zJ3RD_{b+%F)rXkPfvDdzia{0e3*4gP{WervVpUYlM0cFj61LFla`yc(geTy`s&f1*T1o>ImQ60+a(hc>Ob0A)&O_IY|zMaC=YD3HL?;&1Vz?Tbm=y6oc zWZurIK>R(=A5NCqMvYINj+Y%02?)9jfp0;`xr6_ud}$w$ifzyb|y_OM+FkWZI5xjae5qX6?z8 zO}H{T&)*$9I<9XE@ey9NqnAaxT$Nn!K90^8Ute5}Hwt@J)Pph6d+byPYm?9y}LJu;w-LM+!|)J+Ipgo;dghz2sI} z=>L~*(5^qto`&b0!Y8sGmbGWf9h>9BeFFPODDT|2?+*<4?beV#4Plubz@PrJaL0%b zybs{Tv8pO0_Yv=X@6zMVCmf%$j3e#N0b|G zka!FAvGorT{s<2H_#>!DkSbjgN z?otoTo2Tnj>iV5#H0d+4n9?1?+&k-XG3ehp3r$xBf_azKr$$zb+DNxV%%9PL{tr5? z+*cyzM@BLs=`p;IHutk^18I;Vl#wN;T7&Zc?Nv1o>V>Z=e>lh!@y;$kK6rynpZjsj zxZ#AM1S96a9qpW-91Oc;Iat3o?Z*G{spS=)4|UPtK5+S^OBKS=t5d%ZRZ`)+Pmq09 zR`dq_+4$|nX;?3-&$L@2LUAj5@~77f*{7Xq{%@Ya%lHQ_-OXvcSpU_NuU=R*KhofW|lUmhL;{dX;r$?E=M@7NxQ=lSfedOf8G zU2U&6QM6v+pF(@(O2&#S+qaxS=OsLZQU-!CrG%?LA>M-dqG3vXq$oz*gLSeED1R~O zfO%vZg=@oL9+}Oih`78-@0O|g6)&gOs+xj5X5hVX+qO4;vZ(zcRR{KW5>E-~(oF6* zdi1)TP^KD2%aoI-!?{p4#JSGROM&{G$j-(2g5gnKEfY7#}$X%Aq2 zpMd|BE0STRe*^X+|2u0y>B`0fmnuBCfYhEViwx?tW#f>=bOQVxFV=#eg@4~cn~ z){bF!WQXsX3|ah2WZctQzX{^ks2zQz0(5^(hB(8_6mJ`D>0b#l4@R`0sCTj~=+s zz_*J1LCi=I5}#-r3I%@I&xKoYtL=jh1Rd0PXovd80Pm!944lCa|0{I~-G6eX4TE8| z%uws}2pz>c#t}2Nj4p*5VP$?^_Ue#5ZXxR)f1itLmBO4zfJaT?4;I<7I1+bQr|8Xy zFJkOt{vtM^ZT)LGLoxV0xNo7s!8x)U)h85 z0X)oiwRd-u;U4c|J_5bqvZ|)n5FcOMq0VxGc;n~CS4oaycYBtuyeoFKh4;nBby%sX zGxrBI-m!-D0=$D-S=6v38tfZ;8{$`p2fRy?Zw-!fsvCp_@l=kV`(TIb3!D2NWZ=9q zunjVr)?LX8ci1cP;r=I2V1qJR>iw^&qgLsFC%}B?m73OH3-;;nLG!x+-#z(-Lg>sd zpBX8$0DR?Bczw(PlEaZ_YFo}90DA)XNbh^I7yC3A>+f)o|Kf);hlk%5(bM}fMcK*$ zcEmpDhlTn}@F?`3qx}j6|2x~6Qu)Lm_^?j%ypqz=2EBn_)*I{S(680yNs>qU{j`*K z2;Zn^$Jbk0liaIy|Ezeh8T2zS%pis`8#S4{^u=TZ;lG%{!57wFbCoRgzh*t!@80as zY&;G6k&jFEZQIROPHO(}mv7P%=v!aD>!FV7F_E|uN!?58YuuIG4y2`%{CUovm$Zz^oT-Z0Ky}UudTH73;fg*c2oSAzVgYV zpGrxWrlw*7u*k_Zh~I#R@q2b%3`fk1c0*8~j<~Bq^0;|kGOY7~)`zu4{)V-!bl#ar zg*|VTEDXY{{BLfrfc~1tL-UXK7WRVqeYMS=M5M|rBYCWW=i?e5qyz`cNxwVePG3O% zp-l;f2;CEXO8@e#Z-;AhQ!};r`$5qu>e6{kuy8X*Nw4cV;wB+gr zVl^__6LL{~E8M%o+NmK!$FskcYzun$MiGCAiwZ^frSypkAw5VkHt6CQ5?n4sq9~yJ z#wadmpTV+qYTweE6HGl%4b6>X*xe(ut|ek6VgkG;OG~c7f&T2Q>dRw-^B!W}rB&+K z=B2<8%+JL)xmye4x>CAw9uZLd(BczU6fORyUn3<*4iDZh4 z1aP7T1^7*@6tNoPcM_@jT++LT@7`y`cT2beCTYmq^ z7oeqR@G|rD!kz-aKOjCN1O|g1(~#EGv_adj&aM{7oylR8*T1o9=s$4c7)MM_SW*iz zh5qH{*~)=la3XaPYjqS6YM7)6>Y1*0?fIn6=9H<~l{$!T<6Y`Na{u_p-g|STp2OgO z*5Wj5oDTO$B%c$rn87yr)P63#Oxyu-RtORx8N!ALyR z>mYv)RobRf4gH4h_CBWG8bi>ZK~@n`QBtxOT1>0`KmFQlrCK`72M>nt$H^RZP{02w z;7>(i#NVbRmcM=6lcr7EG-Lt$3Ht4peY3uV+GcltmBhvhyDZI|pWtt!5~Xh5HuX$e zm__m9F`?hj{Cswi)_FeQUrTQ&fIq>!u-#n@t9{`s^4=NUd9*Qv`MxI$&C6)%cW5}$ zmcR4YbBKQM=OX9nk@#hg^aAu;`wv0?2EV0G@2rOauk#~0TnyRnQF_D?={HQ#hczt* zNsR>yz6Aqt{fO*L=walt+PHAhum9!UJ%cQQ$g=?ePp9@6CvIqK%ZoP z67WQfHg7)@p8e~%rAIQWVrGWCx92Ff$6`~~9`}^NJzPHDM7T{BUjy|D`Wh`52q|UK ztGoj9oya7OoXg&b=BIqwp`(w^D=m>Z{Ib?Q#Efn=C_L5%Rp4R`>4xTKVL2r$8^5ePL|8P{M)4FDT3UwVyk7TULuw$hUoGL(b8dt2@+?|- z5XwJk-fy;oR+-gDxyoYnyjWb(4r{Ulv{S+7XW;((UoQNjlp-(W8A;QDcsbHV)1GA& zTUtiU??>@F-Y;qZSK)Y#r9^*#@_B61nn+dMDAOgy?}76=-;yY7>uj4YGkT-qhV$CM zD$1(+6%cRze$Tch4vfp&kITE#u{qsA?Xf4z(EVK?9lxhuA9Lr!`bVNDzM^HVq)__r z6v^aIukaPUS>^keABs=*Fhe}#QT)S;VAkt9Obn9W8v5;EpEDcOaj8T6Y1MASRcJr3 zNBj$mxYtslJApQW9A zC(^4ZTCkpghZ1;uGQgiff9Q9rRPqvc=eC=o$X?NIRY_hS*i5M9^2Fm;M|JwLycM7x zzf>(rDR4GTvR+@8UHAPf-&l#cR~Pa7BEw4p-8wT~THg;6hxPmg{SBRcB-wvdxaW65 zz9Kt>{p+Xt`aJ1=-FZD&AI%{wY`||@^52amt_qOP&6`}i6yaK>bTe(m`rNiHubol9 zq)?mq!m+c@#0CCOUcBOA#d=tl$Pr|pqvB#>p7b2;)YAO9Uyz?V&Q9J_|Gd6#*_&e* zM8j)7*tPpFVVtG)`DDl#QU3L&K6*#rMrwQI0$SgNoKX@L7dk6hFuMZs!w$N?_r0VF zA*19E5#}gF;D70{g)rhX&2| zS&6lpeY6b{;H%-*_$>xgU|`9>brJ zoM5~Bvn$91n?EKy_Tjq%^y_0}{Wq2mCmE%F3cST>D%8_Ke{GWYX?H{AlkM57-4D3g z$=4x0_F?^o)dGAXF6fy$i>)>jgNsM^$D~8n!LqDVivtxIG2LAwy%Tw(O)p% zY>sd8&uk-_pFK`?E}x6ye;-w`zY6yDVFx~`oB|c%yw*vnX4t>4QND`{3&NI)YTI~` z$Uj7uzNwPeHLvsfcY^H_M16TY)cf~8 zqN3>DKGJp*LX7p6EQukBA!Qpv(n29x$Q~oNgi?}SWy>-%28j`3RJSrF+4nMauQHNs zjcpq5?|IF=_xJnLqcGm@*Xx|;d7kHa9*A%A5kZHkWx8p)-Zw4_>UI2$@e&Tt@xXxwTP{Z@@p*38&HbQ<*s!ts~$w3=`z<$&?BeKTJ{a z4&4x`dA#!r1>$AnZ0n)Gq`Mve?ko6(dZ~&>hx0MXFsD$ej+GB#LP?6b27d!Kr>bhI zQhU>=j$=@7jgi8y=SSbZMs2q?ZbJ0~_+jOZVZ%zv_cq-U@V8-!G1fi5;q7IEy#Q}F z^Y@NX%dtMgZq~lffRDz&f3k!U^gK4M>2?Of=lrM4i%)cq#Ru}{4qL7*Xe`w-@I!nA zRmCR)?&BAjKTMFtRZt(ge;^}%#GvRM%X;@@Sf_HlP|h|5&V!YmLk(R@F|Co149^$#(fKx0)g*t`Pjtfb0Y1PrD5A@9r{3WQ zQZ7+{rt$8=etP2-wrTv8a6#j9hsq)WCFtH zVc|rzk37l7N9$_?A>PA2Vg4AUku54m_ZUj*w)vMY*k4r_Vh#K;CiVJFF;nvgO~}d4 zYz5r+NxTbzc6;ApdbASkQ-_~tBoo|SdcF0xAIyDTNFRVt``%1em9#qSKM(w-^}InL z@R7HN)BOINlY{WkcaJu0=ZV}{Qsg&*^i&=<9-?@3zuY;pXj;aI=EKD?hX#tp0Hg8XZZN+U1e zdA7|Ny`k*b^Fc`Yge*OHb;Su&x8DbTe453UkB2c;o^lf26l(2lKzfB+Rz0MWwdBWn z7qVc#{d|MvK1M#gKQ?0vkt%ikhW@RQW)JxMEf60&TfEOOd+%RUME*uZ2k-c+jb z@=HHvkb?*0qrka0fyZgOPEY)hzd7{d_UsnMi8iK3&^@;t4}fA2?@+9^IK5E2>YT3;M>NuU+nq1F z0bd&yL{{U_`@{$uI>eqnl~AUb3*dJ)e$NX^4{_8)X5`I7TgXR@AaJWCtay>A=}!Va zkINq*8}2(^wBo$bnI#lI8^DjzblqC*fO}sSpDG5Gcsj+t;LdX!|L6>Q2J^KRE*&Gg zQNL-Hjr=EIf797_RJ9$#?VA1}eQ-WtUN1kt==O_E^mB}5LH)2(L^$zEj{NZ~+O(iP z_j-5iVd} z=E$-De}yZZ^a&JOcd@_9req-o^e~}*$Pe=qsb(MeZA^w4T)kR?d!MXg`C&-VZ*nC* z5jn!%)Y^3!>NDW;e5P%C>^)4n^bE`)UYJ9?5csj>ld1t< zFMmnOq8t?^nmKe93F@D&XS$jdXnw*Xzv-nK%ec~Ry)N`CI_KsY?)eI>9^+)!EvO#} z{6fJBO6N`=2W{Y+k1QD7P>@&CGg~Qi{LEld%76KwW%VV6H|RVLlH!RSz9-9}KX?uJ z0_QnHgT$88Ckam%>HgX1{a^MSmz$g zsI+&8N7%jv{)L{ltNTY&TItYX`>FGE&<_@LtrdDWv6UF(9onC)pP|l#c{6^bOr8<5!qye63L;8D#>2(uH?7!p?9x}8d(sZNh zM)J_7Wz zlNh}b2lM#j>@^j4cYRTb`AP_0{>~F>>tEj9B+xIP>E&AIaqjwA%A825u;c#Ycm4j6 zP^TrlA2+UHu%_Cc-%?V9-uq%nRXzXa^X8(o8uv8NM@(V{!a@?pLY0X%N6;tiLzM7hF-e~>U9rJwqz@Ea9&$X!DY;e-; ztZZRB9xj-KdDxmkd^qgK*m2fy@SZ62w_rbBQLqEcJ|V6>*nI}!{l%kUM4H6+035qr z9O=2k4({La$4#3Dob|>h0Z+|^6{3B<+l^-OH*YzrLp0lXWDoK)<}V%71bqPm&Oc72 z{i|tC#h|`}+>4Ui&5+NIw6wIK{W(zoLqFaB(NL-F*zhEKrHsdz}VA;k5$yOCe|=$`%7 z4qenL$FC4?=4xvws%mQQ?;kMvXz<}(dl_%^{k+toN4L}7E!<^00pDB>{eA`J?gF+xwybl2b0DbF(@`1x@&HeN zo(VvDVTO!T-?OQToWcA2ciNDz8p1yp++o~eefzCWTu{HIt)#R><RhAwM@pp@^+*ha0}qAz29YDQtXpD@8$Th4!HT;^9!mNPQRQ8=yz!*ROI4 za!2(I&qq~CD4N5s9BEEO`Xy4Bu-JxU2YxJ-_wO>1T|f6X;>&A<3`f^|*B0cVqNlS{^)E-cW$TW}3CxyPMK`+4{7E1DyG#|yF_V_EZeJ`hnY zX)5w#qWXE&PobRCerI%>oOX$!`mg+FOB=RxmF?cYR|cGIb|+1a&)mU|hzP9~4YKoc ze1+yyewJW2OQC)M|F!&*l9G63zgKhKNlrHZo|lITO6yBFfH%yE9oGSG0Di2#EdkGu zNlNXt@34M;)cekySMDL$_A&qF_jfyXB&K}s9fbZ&9-H!2h7*Ga2MJw*)E5be$YdOz%RCKED+A$zLvo5 zF*FZBN+9gea5p`}9_9GLdFjWe=d128OKeZ`_h*Ct!#{44<&@XL9cPq2t%iDtgPqB@ z)*$v3(T?6(1pl2(c)ldt@)xD^rERW)c~b`P+qo!uDSkuq4q>VqxF)+-nkDP&1HF~} zThDa9P6oZ)v%+zeoFqH*fA#W7H`8_JH6Z@9VB!9!cNDl7RR7j0CCJ}r4$t-+OG%cC zU%2Oez}?inXG1lr_wD&dpx**|v4dazJ=R*~%XK|$MEpm+i#)&AaSgugP?`|ba~v!U z?248>V}hC=HtS0hjU76_bP4>9I`-}betMB1V?TCa<2ILMeyknvne+8EUz?xH-+VA- z`4H8IZTR9c^{&ND@sIx(VJF!CnS&WiiL|VNeW*51ImyopXv}Qz_leB2=K`K)B#-uG z2k*({U|m;J587TrpXYjMX>VMb|JA3<3UZ;h&v4x%G2WrW!as+$p?cd$tq|5zCQ_z4 zZYA2Hcz3;}tIIB4=e(j18~T5fyrBH(XzDh@Ij+0BfG?@jh<@*U;Yix4JXv)A7sDcJ zD7e2azMC#Zif9{+gw7;RX9zp})woSoSR0#2`T&sh5h!P#+R z72=bvE?gRb=QS6Z{?(FDP}I<|`3}ObY&?M`XH(OhrY`o^Ne(B}{DXFj1JXl`W0Dep zuh^h}9p0rjq#N+#&DLh=AY&ItUGUEW|2DwSaf?}Rs1)*E@aK#aPW>7M=b@hzTK#K% z#{Qzj9U53}6&stx#^vieSTFCcGKKjI!$p#X)E_syq;`2_llg-BJXo{KJ}pbpM|d54 zKG3tgc_&9E5A;=c;k=OF5zMDL2JLaZGws5(0ek@d57oHaI-#l|UIZG#+YJjRPkg+( zvGJptPA(tU&7>+m60q^P!g5J!>dRKz7>W)tkzqEi~_}bD% z6t_ITW08UGJ1M=a>gS~s+v$=dP1{+(BZZojr5yHA_5;xa5bx&d_cB>Djm>R=%8%E= zc>(_8T~}AfuQmRji-Q6^O&Z10y7kG<$QUZe1CII0rd{$~)2}{xht1$pR!g)R{y!wUNX|dp$ZwAS6%;=XTu{@$f!jej<2=l5n_L%>UwfsCQw0SX0NAbux$cfOFdx<=ZSJEu$;ai$3Rd;Ql{> z{_PkwQN6|Rn`i%dZNQK9I2w-W;B4Zxm6~oMUwQ-hH|&y9TZpTIm>m3n4kI(@oY{^I zLe*IlbAt1JPB{|Mj%#8uLhmk*n8l72!qk0{TZwG#dYlxG)B_+LgbGp51MfA;^cKZpyCPB*D zKQ`SNDhK=He;u5e=Fb}mw|gW4KLviMh19*vQtkelyb$2u^qqTS!4C)YwcMzE@cudY zmGLpze^pTA@n!L((}KEfNgQ+7K@n zdo`)Jg0qG*ZuRSdFKJ_hci>o!G50e^7vTAVKCrsX(Xr9BJTw3CdZ*03Om^rc@GFOX zd7rhGkv{~UY4aqvxp@0hZek+R=fb{wHFZit*@=W-%pqSm^t~yLr?r) z6G1=xOPP9*hm(%jv5{$SN2GuM;(+4Co9RY%#Q%|WacV?&mq&NAj@*u0NbV{q4@Lco z2t^jr5AYDYPv9rr&*qE$efd0!2Sy>wDfvzNr53+d5(M*$@mxZ+rm@nk?!CabJ>lRj zTF$ZL;ydZ{Bj?dRfCwjwYS0Suh_?>Wz_|nb@=Cm8M@%(Es?9Q%fF9luJ#`+o!T zn+=qwOMh;CmZVVz=WQO#Xk~#NjFXg+9^5z99NxmUqdhJyEwMAM0iCagkasLf!j=c0 zUH5I!2EQv(cvujTU0ydvJpMe%ukqi0Qah#9%`gh(iw1JFdO|)=XU*}E>gebz7Y~pA zg)L(k<5SXc571+_kz zP-(A9&rZ-A!G6E?f}I^Q@s8TewA zrI4uRgH37Mjv~Foe%YVQJBCw-af128pL%Hrdgm1m2>4NRr~_sBZ2M&roLX)Geq30@q|;O2C+ipR{gP4%B4$-e?cL$1-^4#y)}*Fp zgXgTMGl6(D7gkh;D>%8WyxKKS8J;%>Z=CMhhiDg`$$gQC-oSeT+;`P7AQ2f9$xqH@@ z91{8V;~enKTCjhm3{v~56n{^5p1yi^C8f*<{B`G}B(2eW8S4knr!_Fp?0JU&u^CPM zx{Ld=%Ml-qMXevoqo96k#OU{{S93QdP7FxUQ2#Na=U@-L>X`kBgL4O(?q7U50{B4k zjL`Rw-@klM>iAF3+gl`Rs?Rp@^2-wFW8~!CEQM2u@OkW!knJ{LEahl4NCHNDP1L^>4~}tc386+k6X#t;6gE{!wx6FfRLZO3Wcf6y;OH zn>N-2=8@8m`+sXj_?Gl~=91m?XDha}koVMM;v8dn=X4}{!2RAf!#a3=bM4q@N2bH`49IsLuW8;#5emES`j_G^d7&2@p{msld>Xh5=9-ha@~g3Kh<(K@3Bwo zMtsWGqcu8lW67 zwC*}d>&Z46M&~=i>bgW}AY0KU=C`S4os;Hn(ys$Q|IKeW_d!l2}{k$YL6iPhD6jPIxnud$rWbPr2d-sKQVs^qP5!bC9cCx$WH_K8)X^}&rYRt z^LDQg=*=>x_mu0*T|P1-Z3**mFH31;fj)?A#XXj*&Bn^-i^;E_KYwLLB=h?@v5h0Y zjIog3kxXR~5#I#<9||2&!V(;LcihuYR{F|XuXq178p@Cl`bI{42i_dorNqrN`VTh- zia*#J)o2x~=Cngaz4pK#!35W=fdZvvrns>18TxsPMOL47X!LK+czJ&$L;Ifo#wzB$ z&lCKm1!F1b$3y(G$R|IZC5Y@IY@J5`KkN;qDWBTD>i!E{Zqx9Sok$<(|MNAzZKcrZ zBO0?28uyrrC)qv!moM|~TA98d_#DPuWF~(vWp(p|%u@!PpzoNgf78-LX0dCh zS$h>T#E3S;?hQYXzhkdX%Cjl(vx4)-UMqBP(;$gZiTGgf*HcrYns(M#yZ6EK?T<_- zq(&`|C8#YX2SPpvePTmhnbdh^;NDe%sNXF z9cCG+dM@Wte1-WL>yGi01p1DMAwhmINW>{{R&1XVPJ9o)uZ>jw?uml+*-5uGqW%!C zxnbEIy;fA^3q^(YaGznHyN{R>_|qSiD3^=lNC%m^KWX0md6r*4`ynP;CQ~P8kgn6BW$s9_zql6Me<}w%I-l36hdw6K&lHl{6vA5Pc9KXH)IfIWoG4KDi#|!3j zg>z{P$$r%T#4?nu9r$E^FW!K#Y3;q9;!;8Uh`b|D`~mlO0OldA9XjGmZoLYPpSmfy zFL`5p*&`3tFP(z;)JKX}DsjZdFG`G5YQp>eVqisdvGaL3r)g6uT!HHMvn@@}RZaVS z{Oq892RvI?W@Xk|m}mTm8{qvj%e3$DX4FqNoYA(ELHQBJ*`gRZLwM}#>ZY= zBJM)cP>}*0RK`_``rA-I*ppr3Qe!VbX0#DIK9Zf zx#n#v$I(hWIxRcVh%kyhg!-tUW%Mk{2mH+E1a6P$Hf~n=K(uwX^;wF}JlQAW zDELvkIeY>Ce5~Q6p(f;yCvU)imp?zho1Do4-+h=T!&b&hI?w-|R-L6)VmY7kyZm&f z*&G%@TEvWHkY9rl*@alGqdmWS*@X_D4-ANg!roOis*F6kn+DHk0QODeSgh&z?8jT{ zfWM(XDpQ5?I~z{6A9qK5>mohCVTWp}bSc$6SrGpdm{(|}t}9DdM3lnMOa4+`oZ!>7 zt289kFAI0S|D^oj$$D%uCg;!TzgKLM_V4hrO|3%rb6E1K5Q@k3MKggKF{f6g+Lpuo zu8*;`OA*Bk?HHnLg>8KoA0kqExYyb&*p0y0zFDvS@-a+ z?o@-!Q^F;1{$1J+2J(6QhEe-6O~@bMCzyYzM(6FB0~&wshWdH{-|XAWWXjYjou9{> z$g>1~>$zA<`I}>}oYDEl(m-I~)}idb!wvc|fKP4|u{2($RhhU9@zBrXg);|x)rSo4 zm7Q8|a?&au@q424w&kx#T-RLv&%nRDF|vYKYwAi~1iU_PWI<7{Nqdh|bSPTZ2fy+X z!G1~LCmpSx!u#td(Qm82ui7Lz%`EYT`17Uqx(Mt;Owjea;|cL!ZvkK1x{m(#CY_)( zyFPAh;U2}QjIH1KAIS;q}so7h@>0JP{?B|Bol*rrrNn<#{g98uDL1-x2y1yv>=3 z-3v)j56w5!EK_m039)g_TR2FsUVo!VqVvk*v~!C%h@apeMxb>QC2!Z2?5A*SA-=;r z22~^R_yu3E)H@1(SI)2x3-MuUQtDEiw=t&aYn*_O(c%&AV@0{^0%KZNqezSK<&>1{>#FM?vH zrRS(K=Iodqy*eY`G*fFc()UH(ZvFE$oX2%zSdJ=%;_ATt!TpNr<;e6(6Eo`FjaOcj zHZ=`9IHUY?HZ5)ObRP+@;kZv=5+gJd)C4H9U#wjA~Csz z*7rsoSHe7%z%P$Td2^*)K?M4(P_GkOS=2qU1Lq?<7~ag-U6~g$!6hd=(v;=YgU%oL zC8_OMcb28JMs8X5q`~o=0e85M_?HJ8&Y}A=$mwr%*h@pGm+4>B-TivukY-KNJ}zsgF~zjA_>zJCF3@Sig&s zMY&}|Rol|L)@5kZXx9|IG{j3C@_gq-9ukYA;*=n0)i67{G@xt;{`ht$=-#)hW zKGLt?T?w?bW68R4j}8hxzX82$*DXy+^@_>}r zTz`N$OrPqsnImC0aC~gF4MX{jDm*`M)$JluI*HC^>m_JhFF!@PzVbO3?4c4$u#4 z!@_OKQ*ce8SB<(i3i`$LvV{Dg2X#T}my{uY%teM(xBPuF{@c3?hNz!0Kq^YJ|Jp*> z*i8Ew0eI{yKDRjH;Ax-$U{$S%7#6`%y!r^6aSjjDXJ7H1zx< zV}tFel{oRot0kw9p91C*>A;*8OB>dxwVARh1N=V@_7TlRiKl($lj#5XnaWgMBWN+; zeYD}XNUk03meKb;*OOxl{b~a@n`(88`Y#!Q7z=xIc3ZPibl^nv!2@ zNJk^QG=_zg9amR5hc6?f z{vyJo%TZ~SuBSHApQg6-bayK5C>PM6baNfZmtfh12*y{SbRpZ$RFEiqp9S&pcB9qQuI4{8Whsozr zS%-fqWXb~mp@QE*tbRa?{~G>>;q#~-WyI!R6~P^UboZA#-71Q97B2sT{yN|b-`Xl& z6h9VI?a#4#CU)KzQOgtPE67G$VuWOKk~RrnYnBWmytm!p1@&*;ZK071u;1~3M-TT2 z;4el2kL?VG;9S=Y$%*T}R^8pUcoX^ohWXQ3UZ@|>3mtHxN_?9mJg^lD z<)l?o&imY4hUZCU1rd`{|G!^dx>gTC&~J{rKIO3D5w&_#;xy!s1x!Z7r{~>%spZIs zIW+Iq5Yqd$R4Q3WMJRa+^$+XQUKdoWAHb8>KlPXS`}0Zvo#!LJkH@*=mp=ht3imr$ zlgQoY+A!ePvkJv?>}hL&-rC;PoF?NL#3$7K}45`zbbRt z=CJ27!tY<*`gWoDYF^Nbg6i(WjJ&OB*HC_deK4(&M-?TxcZn-yzN|dYkqQ0@=Vf;1 zkr+|Sg=AJq8S@ZgyQ&WpbH5Cn&zVXa6AP(kX7aQ}u`EH1xF~r$0XSzjYIj(!X+EpOGIg6+f zYrLofd24#7{CA;zpF8cY%v`$sw<+S^3|X`WT^%7c4<*(3rqee4z~^B~LPF8eu5C(j z?4x?0Zqxs_w786?Z4R7lhx5Z|PE07A`IuaV>?r`+LK10NDK zZ60|M@*{`A3_7w(^6#AfWIt=*69>USJJ5!vT$}1fGePsgk;So;vSp&2()qSszl`me3%IE8r=cmh8NS#;_-Eni$ zU<*7T3?tQT_)2^#TA+KTJ=5VY7)m3bv)_X74Z}->>WCGrnme(54)V(w<60R2v=0Y; zVuPWcV8FhTd~f523HXSgP4dy51)6)s;XuI86VAQH6}&&|9!ks9N{uPu9GWfaH=Y5% zd*=Qoe_G^jLH;fbCaBTQQx`=P2vA>t;kkfZ@~=PWBWv$N|LDm8>ADm6|Cp*=AK^F% z{ERJ%YNSe~?BzWIAm3Ze<2UM3Q|ny}{j~-FkD>j{h0m$DThbZr@fqlO8NU9ShpS2{ zejD@foAQazUSGKW;6Fe8Y53X66>$D+IN0Ax?=1J!B!`xMLiwI|aKE`){2Rk|>&9FG zA2U#GZQU{N(f3_c$Hg%zH?|eK@AEplo=JyQJizvH#R;i7x*CfOFVl(;_$X3JO zKQuo1L(JqP)VDBQombwHP~sXRvH<=7d0K=Azz0O`_i@+=`V<%Buj58ku$TLpdqse! z&q*F5s%6%;u4xZDRTN`uoGSZVW_PnDC ziwq`R+F`&0J{0z;sy1WJ1vz(LE+nD;e!Z)P1Z8=+QY=~@6P(9{-z&RW50d3ZmMQ^% z&PU?-jHu;-hJm5F!>+@jF7Dp3Sli?Z7zug^@AKss!{+lP(IV^bmJ;rB5Pwk{_Nu?G z(y~0@k**Tp%|ViHhc)fcx*!&x`!rtZJ`xnrhUd*-e; z9sQ?%BmHbSR&Kwx5Z>P)DdjMs%lbxhTyN-Y#P3DcC5l*kjOtGGw++Gl;NW+zRCVB* zC*SpJfcsY8-{9MDyKK@TyZb1d4}rd|`1h9Sz%D!TayfjS7R*&iW6y_`MxS@=^;zH~ z?;EbzoH&PFiTW7WC4|l^&!x46()6-1X-Hq-M_8|_q4JX`R-QQWr#RrfHhh|70{^_L!abKkUvK2z;e3xfAKBR4g>8rUJc&P}*(ID1 zch~3>ygw_5|1R=lM77DxyPLA_!smf~Lvu`9waj{VZkLBPnwRjJb09Vz-MU11br97{ zJQo_RV1-K`VX`U+)w8w1U&-?1@7c|S^(LrZFg!zvrC(+p^i=#v(m!Bh?CcOM@XxTg zR4=u9?Y%~d#+WraUwgNoHx%VcoC)_@n%0%^(@bL%!$oV}l)T z#w19Wo;q?E;?o@HZB5O!c>bCPcFCdpP6{2^yLXr6uE*TgC3wGMB=R2VQm5;2mrGJN z!q+fwOR%0L8j6Rgq~C^l9>P$?CL`3J?d}>A`0dx<_--aqb~n9yzZTaP;zdJNtET#) zxriFi&2j?%BwZq!qWOyYjJY)gofpI47HWe1Iy}?gYbC-X5t;rUj^%E?p&>bSrk|5l z`ACy;ZkPxDCTVX2kB;S+Jt|cJzJER{b!EW<#6LDZxaDQ*qm&oD&tA$QzOvT!`mN#1 zW3k--X$lL@Ls34C*f+dyCNF;5)a6#_CrwWCB8{9cd~uXQ_Xqk zDrjU%TK)KtyO`&y_3&(CncND0u{xVTzU zTj&kMXW~Jh(6#Hm{!(?H*e0EzKJQMM5k8OijOF002ltoF2od?&6_Z^4pV|XfPN_K z0}H3Ao1NY~F!Ms?68ImoNd}U}-4r3k^+Uf$p?<%i&n0)3io#i|hXE4kc}9{Gb=AlZ ziymFQ&q4h^{Ee83<{v8vrJgNkjfdyLPP}=@n4A~cEGPWQ1NT5bANdh5=3juya7b1# zpPbn`#iRamGkD*8Akt^mUbZsR^GH1)l=EkDQULK^zV1-$5Ar}V(qog%tsG_pne@lt z2fS$^O|V}csvg)cuLk`e)@a!ESdofUcFpFI;q<83u&sH>rQIQq2;)kUkRpMb=8` z9$4vl@3KP^N5f|v@;ON?lm8C`; z+Ezix%0=-cEa7=a73JN*b!R^+c^~lK>ufLZ&#gZ@U5EU*CrM}X?H$hD&FoP;k{#XP zCu!9;jPNCcTzaoupl>Yk979=MxCeHF&kK)TlD$ zrTa@fS5KWN^V;nb0rU2F^R~7vQ;%UENn5$Dl#t1T`j8qMRDa@O9>6;2*KO}k;|Jk= zfj*II|3o7BMtGR&J@ohD^_MV>|f+5hVA{fc#rA$kSiQ{JPp{71Kc_#a8TC=B%m#&EK?Z{fs0^7*`X4&5ixjZS;( zm!=Bx*R|XZ{)y5na8=T7W9s4q=J$B_& zNe=({1xehnImJrPZTbuff^$w>%a-G&w$!Yqls0GN7ja@y`>if>-5mP;cs<`1^*hIz zWmgCguVJ1!WTnrZQtutAO&QY}E%|mi>YKq&YcXQ(nK|(FjIX3B%Ghd=POH1cmrqS4 zL;rpG}6_b<; zt}zU-9f$pHK17i2{n}bTw8@6^EyjHNO{eZpJlJ2>XYBy{S=mNj#!*qX57|^!MXVCw z#n|9IB9tXs$WbG{_a?W-j9*@7!~PNDuYuGU==XwNtqcbPYzHdoXr2lBrDtBheA#MK z;lIO$jrN}z&3{@IJL)4|$JtyDeB%^%a$_C0T0)3Y9DBV2(_3Vea9b#%{W6cYHgGN1 zX17tw!EX%mvAi1BU9ew_Ag(gwKJcJ7&do{Me zZ}74e^;(%|m7fOEd%%3RMJ)I;#8*8oZJ3_A1$=CdS;wHv@EgOkI)0GvISemcRrm3{ zb-gY|9S9%e&oQm7O&d06S0Fzn);xaaVY61Ef4%?lkVaNv0Ka9AHL-AL5nD9EYeT~ zyvK_YPWf7g_+wsJ(34y08P@~9oe4nkGa_D0eS-BR?DlxU2RdBdZf%DIWa5s4;cr7tlez?=|4v>$`atd_hEzn8tJ-6oxd^WoFV@7 z^L4;Ya7@x5>CZWD@XPOPQ2B>%C?k)p(}sGa0par@cfMJx-cE7zuvfWcAk%7kFK!& zO!^D>8!8h|w_<^Ja4+pUZyh=x^{SEyl*Z6v`Vf91_vH}i+m9so=$2Y7QiO@d+f<^q zlcS)%6uB>lVH^4#gX+9Hb#I~lps&uYD-`rwpRN?CTq?-nuVKWMD29RmIP#C1T@H)n z>LI?!AY{&7y3(=eR&%gJ|0?k7=<+;=zTdx4bHO%YnT=WX;HvkT$XxoeRDiQ7k-zNRT*WpKV=er3OqqsNCTnca_93F@VTE!MN0%1Rmun>fwVKGx3C z|McH|Ka}DYI598s$~jk-0Dmt{^Rc8r;(ztMg3|NSM#LXSSgG!E?m3E|VLiNt>OWE@ z!K1ol%gqk&?Ssv#5ihc)=zs8eQH|%Pk7)s3>c_jv@383_x>|I=-K|Ns@beg6ouJ=m>YVo z^r%@2`b&5|hu!h(=w{n>=UY{n`IY%v|Lkjpowl_o{?v!L)TPkyrf1xj9>=k=)E|~I zt={ptQ6XJ?88i=Ct8%q+xMdJe-?j3uy_`FfRf7VO?5JneEv8KgIiP@cW@(n)dF8<1-<; zhX4GXO`HtX4^l^m(ER<~+M#0*PubWFE*KnkN^vtD48r>uWrQq`6=>di$2^s|m`etK zyZKQUD>HM86;3YSLS?fxe5W!_eO(MB1@I%IP(O&LN|vA;H+-)3<+UK+@oL3b!}OFG zek98V&6|@FX!&JnS?sjLgYrp9^ndl!&5tg5zIoW6n^1Y;_pdEq08iL;MQ-Rn0Dhc3 z(kX&^a*7T4p23Z*!C8`7{`2(Yl_m;X#DllO^PHUD|8Sr9{Y?n3Vxi=gBiCDGyc6e6 zqI?+^6#V>V)i8m!^)*4TpTSqM*~4Z-h3dwSLJ+^%jPOF%On~IYdo=3+LA~QDOsANR z_MQ-Jd{!drZ{pU|1N*~CjNA`pf51F_Gd@;%50S)vY?N&#4qXqR`jl!$r7Uz)SY?Y|Al*~h2sBw zRA1@Yol}*qTr9_$FC!$C#@A!|qyJAjQ&jrwScfwG^bqtj^jh%6;19;yZ(?5>>1+%5 z#_$}`$D#UlGa=$MyXkKqiCHL#{e)J`p2$J^gdNSKVg)&}>N@3QT39{ww_$&^NPhPZ z>Ye4R3m*jcJJ>1E{aRTXIS$VUewdB-h5bN5>AwgyTHkI~^tW&(jv_r${pp@E^`yge z&Oi04tRbD+y0v{5#D@eW8@vZG)`qDvqF-_T#Wvbm(HTC8bapzz9r!4W@iei;_tFvL zkwM*!fS(4i!q}UQzGw2>-CBDx%)R!xcgt*>gZp~t(>W@N*9_u2ZcoblgLEu69`f%T z%)2le_vTU?%<7w=zpZ99PpyuzB9aP6M!Wz&aTnvoO5Ur7i#h+85eIw){gO{^^^OBW z5kpn22(K>&%i0Hcf3K8eIx%eT!spG(YoV7q#Vj_CgP}a!w~R|PO7C(*;?X!y{e=XJ zl5&n4+_y+-$s$S6@3gY%A(FM%-19Mb7{^I&qp16w14Tz9v1e8o;pqrpmoSIKVY-s% zRJI_Wx>Zxgh-)gY6)XlKJ*UAIji??X+n7K|7DfGlutJ<#&9M|^#q!<|lrQRC5+!J_ z(yx`&9HGPCGiMa6N$;j@&h8ni*}^#x`RDO2i*vxoHB9BKf_$bmfOm5G%F=tO=~bnd zh3Yp$3U~I&wc#I!e_lJH&q*dw%9;)5FmB{D%*Z1>SY&3cZJ>j>!nG}<)=jxdV3mw$Y}0_^8b(0zS&^Dt(#AR8LgEL;VDLs?e*h z&nj1|u|FU5LC+_$_$2J7mfc*v-)KrO|KcJnM07U^NWWj{h42C|j;u|4^DArqvpymB z*=N}Qr)QAgd5q7qE&Ph&aYLMHu)|~NoQusV2ata{<|8>nq-Hg=uQ<|=_!WGDDyxen zd@+R9nr3?u_{oF`HCb89{A;l93h)W+58TtO&i|m9&@T=5s|)6Z=VrPO35hD&k0XDG zHpbpW4$+2NocPOw4ne=a_NtRsjZ1y1a>x;MUmDKXkDJNYE4jCnHlg=hO9y^Fh~H~9 zdhu{{?ETxO;>7tJ48t6Hde*|{VUzZNpGwcwqQt*B80rT`o}si@_1xR`-gMhr;A00# zJ|8$u`F=O5_v3~872WBl?DgKF`uzpMMWs(hwi9F6lc_C z`TOV!*`GWoA;{;GSlIO#b&s=phiRSbP``zAW|x}vxMJkc)sofd=NtMeS@&q&yyn+H zN(X*r5}(QJAbOA`0xySJWnNUetbto7HT+v|Z3 z06mQ`E~@&qvj5g24+ZxnoYtwMDYSl=;S=C3?PcQ9@#@LM>~cfkR%scOpBYDqc0Q)5 zDgM8xPNRAri!B4ccheN_Y)1yl&tcVv$6Nns7VmGm?}_+XUV>yosS`U*sQoF|whF%X zt+r?AiY5<4IR_xTM|!%gO53sR>D?^xVT7kaUn_rAXi)Z%aK;G~A23%ULA^#T{YT85 z1#Og%_gHmt-Ia?0y2Oeu@=_<=|H4+ZFOK0W zEJUemzEYz(kY+MUXkO~WNUmIkw+bZND^NdiFeMzH$aTRFm)fuaAI5V19-A zLLTr46TkO^?bwU+>*$+Qe`H7nniW?lUq=1l_>+Tm6|irZm+_dcqw~}d#Y+L+i?wgw zPgTqAH{CX(>Aop>Xj49)F}uQVs0NdsvIhPCoj*7qBp$Z8ZGQyyrUjQ5YX$Sbw^zmw zQl_E)9Kai@THCxL$3JM_+JX2MM#wvF>)mTayc^y6N*1!{UmlN9K>reD>Jy=d;!{MH zr2L2DD>i*&)kOoIXv0#=x;<35l@evwA@cY}a1Pw~B@$+T2k{f*gPT0TI(%doD zeCWJ?Shnq3@Vmi%`t<2WIX*ezwcL~j%qK0gFpL;ibysn0PDI{^k~x@J-^LR*QDDyQ zMDdOkuc}Hm^{BP6GXees^somt3zS%iDJtSq^(bEPb2;L6w5x;(+KN>xgN~ma_bMr^+YEs~Tel~|>yfaz_^$}wW zSxDup$S)nFK6`|pVSba$Buq?{*@-Hm`_%9Xs=x5J%@<{Yeii5wcE(saMDCEB%frv1 z-*0qQ>Y51UWp-_V-bPga85}%M>Avv?enA-jMPajk^0uS-SkCN=oS$uX^bnuTNYH@( z9{a1t_FScsg=GCKO{+g9c#%=#S$rwrrzWw>BEOhCpP*Jq6ltJ*%?oSQp=kav$UY~Y zmbCl7eH4zlC$6rYfb-Xfd5VNx;ipQ@$x|OAe6m=GE4+RG{Xw7f?iKnAS^F}ynDF!Y zSNa+b{9?&Tds8yOIS2ZMD=J;H)^Uzrb*H;(Zq3avHH1aG{^F;UfL9OjV zqDZeu0)L7_T;c=nQ7taozl))E@kZU}>*oFf0<$K`ogl%sFUi58M`vbRqv)t*)w8NY@ z|5Hx+QIov;jh`{8pC)JG1^el6A|m%P$To5t40D|CiE{K<|xy z&(#|Zfq2*4Kv*PypWMCb!E?hY)bGaY?I#%SJ`!uQ-x?7=Zsgh% zz$#h0tApf|u8lFi7}cCwzIs*Z*P!(e;Q2y5cD9Jp{p6I;O8LuPCh~8`(EW22BfgUR z`7=CY%G1o`1zJk1M+D=D>m~l+w^LxE#_xs!bdbG{w^LZbyd?c}76acOz(MitU+RVM3+-pBr*hRn*ZuUqS}1nB{h2qhO8a#Dq2QpV|3MUAlLT z*Xi$z1%3_i8-qQR7ISX#e|Bv~=dIR|Sz=r+Nz_jjf#2jHa%#Hu&8t}o z@~1W`UFw;$@ok~C{^t`iv$wXVfu8kSh+W6+DHgpy|FXDuZPoe(;FC*B_as6;k$Bz& z=vN-1D&w|@``#egX7Y-&SHq5OO@jS`ekYSrlEyDXpG2M7;Qwaoo$w9} zwYy3d#L#{1fTvThUZl}=`~Nuq;f5ZXUsvlF7*a{^?^gIev1Ik{kG5Se0sU5}FI-Ej z*psvUJ)iBWs`Dp2)=+2=;RVh=wA#pv9=fuy=+r29EH%5_y&vS2I z>Cn3GQ~92R`47KD3ln}+?uKp_`0t_)Pd+*+`=H4hNbxa@O5;Y!+k*WSlAddDgFSPs z*Rcm7z8tT`7mR|!2b7JT1(_ZtiQ|dJLwxc%oTf_jjchHwS)_1?QRT1jK5qDD&Eccf z-Xd&ba;va7nx(b5d*ZGhgZWBTGj10#2w z7s3ZX&!pUP#MOvd(sOW_cfZy1bllHx*!lA-=ClklCmeS}{p`l4FAJie4br%F8)TnU zx@Z-7o6Z9bL2T~}UUg|m4sK}W7$%~4sk*YnLO)%$(KK267UI`YXd-8Q&3V7W%YWMk z{Fs`;5~Qy?b#9Xb-!rA+fBsEjMX`TW6^cir(q45*<`v#4GhLDb@i+8`IuSKIUeDL2 zT!i0u5X|{)d3w@LPHoO>tBu%x72@dh1=+0XUiljMeh#~!#E~4lOn!37Mmvb_S_tZW zz4XN24N|WwDkFOz6;mQHRjI`8h?j{Izi+%8RAC+C6ry?1xNw?CVoHpnl_!7qY77^7YsD}9lx?o?(+Q!#Z z|I}{o&?BMxF6b|GvwAbH&D%vWP44)H-YZImd7anRWIvo+8NT{n}Ld#+*+B)sEb=t+5QUlk5$NskM`PR;+7u*`e~qN>}3CA zB6>cZT6G=qTiB7zoM7@7WBY{IMB8sLKXD~#iigD60^Bi#hhi_6JD#OVrZr!`+b*`Z zSD|0VEnr=by=Ob*w=BTl6JUQLuXKNlD%$65P3{uohqVv<0zSa{!2G5sDh!@p-trHX zfQRjDVHfmtJMOvL+Sk8XA?O_~Dcdqa#1{TkavTxz#q*Y=?WdUMk7~UxzXNy;;^#m4 zW8JQeyK0NoU_Y${!k13XG>!2G^YOR9A0GfcHx&t41^<0V6@5v&b;-bg(F4757;`td zM;r9YG4_7{KW-V`Rkr!qJa4-TnqA4?ORpu%XccLq{w1RUc7$J34*ih9WcCVW=x?#` zB)=5npZV@4CaZf>HPecV9-;efbGADT&Oo`>#NU>s3>gTmEPxYWX`7_{O(2wR`{L=ZWhO4C@>Ci{p_9YVUaALMi6*kj(IP_3C9x9o4< z#pmDWRYQ8Q^sCs-X$Qsj%kTMcr`~>MUG9=lSg#q;)u3Q|`dvLv_O-ucpx?FdG))6m zf!fqE3i*akKiii)wK*Yj6~$^8@(VbYKCieiT!iQE$D+T>__PcTaUzTwc7X%`?y#Y@Qoqt z789>*cqpZaJzhSndey?cd*#`;2;XKRz9~ zx25VK+{Ynyh;)l1|MlI;)%T#^2;$pY7wDwcsf60cH1~-_7Wng%lY5jv?AnR-*KXiT z(MyL(kRPA(KOTa*t&|FBlaUw%Vnb2I$`wwmz!@l4fSZ*8d= z-}l=fd;)vf!l5x_Ocf||_rQOSKs_lU!s^b70DF19IR6WMMcR?Rf%VTK_BrG)*rD{I z9aF9hUVw)%#gX^lc_~yeYp$6T&pU9PbJQY_eU`n~X}ukuFCya^3KgBC;?F%$FN1m? zP0O6mJNn>sLHK?Y{~0{kzbS!nasAgNw~x384?Xt!p?ClN2f_K=$-&@>G!CDz@H7e= ztP9hA(|FWnpSL-=gN5du)<*XAX!g$DswvX;MgHVGhtuVO_?c1O3%dE6+^@~Kd-wr< z3-em+yBxTy50n&)t(v$2^TY1xvlMXgO9t3u>EiwL(>hDmDD8+0A?;umw z$?<;m?AWH8ue(xbb;YL{QIwIA(stleV1!lQtsOdM0!}xUCjoy}Ks_N4F>}1G@sBd0 zo(p&`{z7z4{(HGwIalw5566}lmTt2G{nb4kqL2ubZ`Nndbrmn&-WU47d>a+?aKDr$#tU-KiWZ^sQf^C)5QlhCtGDnMDt%~L^iVAJG7i5#GzncGkYPY7xo<7hchyIiLms2Ld z57gkWPiU)XFfQ5Py|}2p(8kV|40$^^`1xqMS`gshI-l9@F|C_uMM_)zPQ&;2f&O4o zQBJGVscWAB&xL-?s~{(@(0sRI%~Od+&#$WxKRECw9l~r)2^2p-Jz&RK>aQDHd*dN~ zgn34v>GzX?-s1bF=S+${=fCsayDChVuN9q{u-#<-J%nEvb&(@aJg-(*h+}pS;xbK% zt0#dEh}9d4zLzT8u8sL@R!fURTBc?4sYyAAAKKU?jsvsg{_6I60onEB{%vHqNXU_{-x_-g(g%qEiS=O<|a+=cvmR{Lhkq z+g|$-U$h0~x6~7()wgQfFB7vJ=f(Vm^uh%Cxt9&O*?N8`-lx9C;Wa7Wt_f5?e=ZH3 zC;80|ukW(Uz|8b@_na@=Vwsq)O6A(=TTQ zI##EPrhQ**(Dr@?^Hdtb6(e>=zlu^U{FFC?_AfX8=jHZt*sm>lhdt!4Z1@oE!heaP*hcUOnFD zv6;zLgDgqVAJpNn@gXNa?0p;i(A{bQ@K)$A>FnZivmBFkT;ctyh6(ePhX#KtJyF*U z8xZ5KHCjD`j%gccvC>{C_W$GW-7af>KaeqzQV;fT3`qFwkCv&*5@NhR^dg=-wO+?R zYx$D3_g3HCIG+Xj`-?z>^^64cL+*C!P&x;EmAdw+C_KU+2;TON4#6EYNx%2agTC*n z2AMGoevnGb=^~5sVOJ?-A^D1g^~Y%5Xp`yJ(%8`R^-H7WEcUmCmEyWx0HxEJUb)hia4g!u0BhWPC> z@Ncl%6Lxiwj~UIf^#yzpu4B2L)+G2JzPHKSd7RdZ)1l$A z!m_d>vPAcU4@1XhW^^+D47BK5zUn5-*QoE`qb)1uFT`p6nAW|b{oH<^R6Bex<|i4C zANj(*>-*?J`>+V~M${b09tDcnXh1*oM=Qhv!%M-$q{OGYyB+ZUHuiz!y!83a@;wd# z)5xAvB3e1UZ=T)lRpurG{s8kO7D+_sa0&zZYvx@DA$9|uD|x_|uU1eF90L4h1dD6c z$Y1}}#>TVO62pQ;b5j6a^aG}|n#c(o(MClSY9 z@6d$!4(3t!Fl6cN8tn;~S=G4oYjZ<~I6n)`BUfIe=hxr5%D4Rs><7J| zV+e>9D^TTdA{ukMWvkjqK{ea{<-_UZpDZ^$@u#?-U-gL!U%9yP9N2?CY@f-w>9b3?4?p$`#PLrCUDnZg z+!&)@>z}YLN9z&Y96Y4g`E^Vd^dZM<{mq(IN^Mx4UhkS}i}JS!vel!NjjyL{bf*}q_k`~dD5=w?qBveBHBNrnDUn5ONCl*mLG(C4*H4Rw75jB zQcZKB!ClBNMhWSMiRKyydX7$)o1lI`D(88p?u>_q&FvV#1Ax!eC%pG5?NeHdvUw` zP^*{^O4{PpQ?xa&~{W8C2n=`ch zgnX#i9Gw=7qWF#aGLP;k+9W4%15M)Nv@1T5Nm!c@_j~v?$tOjNLk@b_etLxTl~gEi zo#6XMsByXYu8?nQqW+KqJOuP9djy>~FAvhc#%8Ox6T!Ykk{Kl{dRb@heLlGNPJbF) z`T8LY=56Q)?-b*K8HaU2Z*MVk&C3OJexfq+@D5ea4JsC~C(BU(Uq*{xNA#aJ_n9iq zqx^)LUg9XSbGcS&?`b2(e@R-j`30#v-CA>aR6jKcu_3R@OWzi|_KKo-eL_lPi%Xhg z6E6vTQT{>b)9QTlra!*8Vd%7&f0ynw()396LEuIFFr4Qvwed@2a5ckO*^h|XDfa)X zzc}U{A?h@sdb5FHNYzq&=C`md29R$k|`faKhk05>$*3cc- z-wnTd1JyUEYCU$ur#H$LYmdSGfd0^oyz)Jo>*N};oi0HAY6SG4^71dfqusIRGR60& zu!p-NanN_;*>_hU9`3`4Z#(zq>nvdn3?xPGJ=H;6CB}2udw24O(EAGa`v-UbJgC?B zbVEDBPYFKuT~mDG{iJ{Z%=Qt)H{PVp8dbNg2UqU%gZG^gWlGUy&X64FSH&}-oTIe`AVRwx>&#Wv1h$leLE|;!*@VR=X zZB`KU6TU1gMg6S8^HQc$Lo$x!J!Cs)^nSHdyODnGftjd`S0_2CgP8?Q3gABgj}0lI zKOXKVOuRCCv>ok-bBSY@zArH~;NXw&6?f|1+j7>;W9JBe?ZIdMtB-%WYo*o`?cobC zS|}dCs&Mq;dFH+OqJX}MF3>kBz0~ZnE4@P5vMsw=_iBkxh_Bq|aiK|*(;ldYGF}58 zq?xN>zW+3P2JG|BenOg!!&I|uby<%<)^~-6nX9c*H{^c}owYD8Lkr^ht)#d5%pPgLj!@sk!ZXHiYE*T@%PoPJ$d}cor+1&>ty*l6egg0H1oq#o5o`tR}|k& zy1jKtBF9S=&Bi{xf2|1oqwxG>2vrL8W6in!vrSgEH{tFmr%(L-s|q!Ot-EA8l-Je3 zymuGGUki}-S)@0y2t)CJkw5c(a-BeLr|U0&i|bWB0xg_M?$#b3N~#zypi!tyoYODW z_G~`#A5n4EU4Jk;FaA1z4DbS|zkt5{<>HQ-;#afYC?7MZVSb=GZ;VgNyc&YyFZNct zZ?F&=P~R=FgZU|4MI|mP1Q_Lfvt5vX8OH7>xCaIX?!2(r61U_l;C;}aU^At^SdA04 zuuR_e2E^0fb=81&-;Uj6N{x@*XTWPkY~z*Nia}p)2-AzfcQ4pMf51kV&zt-t z?=%hcNk!*#i(1a3`ZC2wZS%>_IqUD*vW_buKD ze-@3p0)E1Wc@lK(%D)R!JO$>qFt3_Yqh&(R>3y5}+2u6k)8p(up2$<h zvfV1)?~U!vdrH@;PK0{JIm@nK3d92-qm^4!B)L28n)-uN==+TlNX%^bM9i+krG2A_kLEq#41W*y z&PPSVYU<#9iO{exmwf?y$iPqj1pMY;L#3s$?~7xa+b zC@1CR*TnU5ZBAQGIX>~~NH2zZz(Vb-?a7(a-cP+8(xLtbeDM7Dz9^qIN&tV(>#e@q z&q#}Iyy3`F??d?s^*xcH2lh#i>@b7y!`d+SzU?L++s+Rb-39#hl6;Isk zmM%e3_IooMCg!)hO5tU=C$EOO){9VnMj(ZpY?VJ@#KI{?BYcJGY(u@0<7HfPE9otY z7ezEl2}b_s?WaH5zLNO}(x9QNO8F+mGU(^uOK2{>K?V5}3BU z{$a?BV(iKg?g=}eE|GA)VcGMQY=m!D7uYb9?<#FCKG6s9G1Ra8S}i_IxCzOfm#xb{ zpNP=wfnRh9_eL_+7UEg9-OOu7I)7x$>Seztj7MATZ+3Gs1Bk09 z0FQ^7;#3ANHD)htLGoReEbm`jOvYFG3{SRM&N5lRp`MbEK<=o%m`>STt zWzUh+>3H+!6zm7g_gGSto3mJb_=)yJZ!^>*6Z8NN#IVzIRl0zG%n=TBcDnH{NE1`N zzDK`KSl*y6_UBYV6a5^D|KNUES1RE)ewDma0eIOs`&LJXnc2ow%Eu3~COSH%v3uN* zMbWst`A;mZW@nf;*XiSc>V>cV*}fX`XXw|Lrguhm$h3090|5W*Bh*BkERZj#@5?@T z6y3+#I4QrbVg-EV&(L7NH+u;$6}la%oVyb;>yyeb0*AfU!#yI9Cv$zZ9GoZ6BiwJ3 zt8#77F#UZTI&U*<=bkVR`6o(=7Zh4iJc>me-ITCnaA;b-#|G*>;}rGC@bL4`3~nc6 zSM>JgS>MrGvwjfftDO%1*j2gLKly4(c)nwbvLX}kad&&A3g~xk=x6Dz*pBAM zkpi^JHJ__}Ty&=le!m%eL4pBKO(rpCK#v^o4dWAPtvp%(rwW?(I^y{+)lE%iE7LD8 zRt*rqys!W8l4bf1<2@X7-Z57X_o%=-I>dHqCOUtj&=$~-TQV{9_NHp%R@*FevhIF?3#QUAI&oTJWl^%szBfTn}-&So}s%z4_vhE?=HvuJG`f_rEO^?x2I+VG> z&)j1<;QwP7+u>k{v)G?+Jzu!qeI)t7#)p$?Iw;;1#mOfocBZAP zN-UoUSB3d^4lkgeSX8grEK;zv2yB7=mZ$tFQG;_oFBj=aVFe{S>`s!6D%N|HBL7B+ zm2Kfy$QHdzRh2^iN0hZ`va|bQ;@e$BZKy{NVLqL7`_j)xej&Vvcmw*44ftfHW&YKy zK5jeY8$|bRG*2FDP@(enKs+?ero}l{G#sX%GS{Hko`QJQAKG7jt~J=Vq0<@RF-9jk zTl4<(TT|cV$q#RiH_Iq-J&W?ySAOMP6W3e$Z0N5NVGrj{zpWsG{b_B+pqIG9k;3cl zfOxN}nPNq$p<^Eu`q~rva&-RzTRcSU>~bjhRIp*cts?mQVG(D!`=&MRpY>$X`VkI% z-?_q3MB7*wL`3$W{xu2W;Rmf91M!!Aln$KFkqxcGgapmrCxeRZXg!;?awo|vx2u0U zYXkV7?U&axv^)lDTUy#jh^KcpQ;F3zbv@4tuY+us7$4BH_xtDR2lYF$_3t2FnxQ(! zm;YfLOjkMAy|>eK0F;+^v8wO_h)6x(0?n6B3X4QqWrPi|4QCp9*P?# zj?U@WqI}FSq*BwudGW-lm#0y@$xgTVVc%uGzpF|#fb@;ALY#^QzdxSmaxDV-2lqI4 zDnb9YsJ5-|rW)WAP`^@9e{{I(a@yK@m^)Pi`V5x5jQ|I%9JbYgtJ3YQgrS$ZGV2rH*c>RC;4V&ec*RuIwpMc+Btfz@jzI?ql z089C+{69W(yArYUKfRbM;E!T(jm2L%L9>g!pC9c=^FaLMH;Fhsz%$1Uo`Z73=myU9 zQ!~=2{z!G@N#J;WaiUwqo#@X5Bduu7*P9g62incieK3lvWL3l$jfdX$B8lyX4{n8R zV26rzON#*PML)qG#$|Fkw_QJUaFcB-oNp4pla3o)?%P~6*IN|Mdbgbhe88EXyh)Kd zn(tKaW(@P~({D-;e+=2ee$|q48FV2pB`}q=fZ~HWLn4kNrB*B`XT>z;N|X)OV53f8Qpei zvdQHG)&M>zY5~)aU7D-D&fZNpDGC!E55Ie?_iLh&%d4z)Fu)@+MDiTqfg0c+^Kvvr zZ)#YR8&9Elu{u(oPG=3Kt2Tee#QANZcI(@1eX7JR1@MP$0%3Io^vAAjR1eLXK;I)W zEMN#rZ=_hR6;?sLYN+1dKt{&bbn)K{0(Yp-0X{)HOr{;yk19_ZYRuf}gY@j&-Clam z|3bA#_zPjBbcG{kC*Rsz(kjlcy<336eQD!_*#@`|RUF{+*^3S>&*tZ4DFHs$3wj1c zvtx66b<`eNgFonF`_o8PjNI(Hy^Lnx*_fKbQr`ra_q4EyJEL^?b7=BsX&=BpZ#>%S zS#FQ=QNjteDTk%cYKrd#ZOK$6?{T(G7w4}7btOwty^OszsD-Y%`qwfR)&u@-gzD^X zrL|LX-$9eAIbY2iFYyyF4_P#0a=pFVyCN3#tlb4uT1_b`V@YrC#4GUg+N{5O_vL*iN~ z79|CIVMF}aMrg(@)r|To$8tXyU<>{xV(oJ)2VSCzR_|xP7a(5qX(=kwBQ5rooB)3Z zdi9{0akH;j*$uQys?;cSz@o)9? zzRZ6P;!Pb6At8jRtJ}Zvnfu-#v7QL`^yW*erwkCFmbihzlrAejJdo9JoTx78YbY;@vGN7{8f&R;?G(~8y;DobnmNW zLOt4#ISU)2na({m>r`>S^Eg)Zq)AH3EKfb8YJTD->NmO6Jf(Lt?^!!D1M(GqH7Uee zPVRH)*3A!3h0o?Rq$ks?px!?hW_acgR8OVGbLjKu%;#(M4up#1(fum+<>~LM4UKYb zP`!%a^n{^Nsr}IKV^xn!Uut=t_P_nK)*YXAXYPqG?@QIc`3>fm_tznP+ozvvU-;)l zOMHG9vCdeKv-KmK$Ar85Uo7hnch8DwvN1czHid0rG#ycg~Z8IV;MnJ40)id(pFU zh)D}<;l=gBs^G2QkH@Jlxt5H>12>C13_HQU2gy|L)Z2aL_!OQ-S9HVs<~e zx{7XU>zG@rhP*ssuYYJk7{&g{N;(Pg=u8yN)!kF?L#mYf#6P}E8Rl6!HD{sTM!EZ? zPZRi3Fi&SMm#lx{y^+9-fckrCGw14xRWy0~PyARS=Dz_x`6gLUf1~a{a38Ch3BY$J z^IPLPmMKC!2=n}!rAq1X%Pt%Q*>i#ecn!)SL2^_Mbm)-gdnrKq#}6QBw^1FW~>qbA}@i^)|+otDf;o5a(;0 zDFLc(Y4rQrE1O+TIXfK#KKaX{(%s_yZT2JXL3o;xn_n=O zUd*xg$(ff~OnK=;E;K^^xN7vD4JoPrKQCA*;L*1#TRHG|YbVK`RO=P{K7Yc-lx~z9-QZ-p9Zy$-c6Lw3vT>Bptq=gSiIeC&&#bo_b@M zjQUToEV5s4SG!8RkJLDtA-_0A2`sENidmGFvUyWpd)OWQL*tR!cnp zILhDZ(eL0-+prfU#f~1sIty<+ubX^Si}We7YZxC>ST}KGe0`>#H`$|mWeD_ZR432?nSszJY2cus@c6@vInq`Welgl5jgwaR+pYN8e2iF6PKO5h*35ZrD`Hrt z?j>{bj@@wHFr)dp3aC#bepmUtUSjvg^nk{F;(E#nHA%UyHk(hs8(M_RWYisN&)Nz4 zPE@l=kIN`NGmhtbRLY*>>6Elf!~FrgMJrL0Yc5}Ry|y)HH=2j|ngT6M?qoafx!K-S zcAoZ{R%9=plLGvThEvT8OLpIc_n}x#2?=1YW{vTI6bdG(`0a*afViLSWl75r?r_-_ zHPHZyujkz4>4V))I4?eEovygrdX(pM7Ul1_pPm|!Uki-=C&}bxZfTM4R>ArKeiujP z@cF_eRNpn}$i7BhZDA+}eaZ1&i~j`p@etsNL^5Aq{)dY3Eg4;kuCtx<8u}DJ)MFT`?+9cphWycM0s4EVqlnDt7BX3R({gnR@X^QFu3At)l3e~!=aW6G z|1d^W-;_%ml2-Nnp$zc~%=>JOV&YWSbcprxU>-Ax$m6{pT33HPE+TyRZcX8tkp!6c zd7g7J_z;Q*gjvS=6=$_}s_)&lAil2;S~hPc-<0+ppG_9Wn{mLsjrQcwe4xFy@~@@g|lg+u}I0rnF<71`qc0OLZjDbv@+#;QM(CYxc_O>Yx>EQ=NuLD8n+ol%b-Zjl^Mg93~GSu_1 z-1Wk|dviI)XueFFYhKeyhM6sWe0G)&^Sjp+d>2Mg7E2`npW;yql~tI(Ie$i`p9?UT6~)EC_4w4))BSUt|IEHvvC#c0~8rzt5S0*1z_J9_Rz; z9L*kh9DwX;y{n`tS?TWX)bL-@T~>M%$&OA?zYx_Y(;rnpeAJ6w`96B;?7s5Tm19Gw zelll~#~@<|O>%!z-30g^^e^M^e@}br^lXyvf%`pEAII6g>|+1b&_7-W!Fuqc!d__* zn>en_zTRbuff-tQBV8#_=u3H?hNTuxSP4-*>_^LGx=i=`J04Oe|q^0l1J zD<3YSiO(;SGiHzOM=fpFV0X7IyP2eBHd_>Lk)h*Cf%T1g)zP7d`m1Uml(fKn#?-}q zEo@ZJt#zZ>zh1gD1ZOvr5E zbdeXOs;)5rdkuUKI+M#S+OVgc%cw{7CaRkK9G`FA<9)*A5wZtHFTVdX!uaxs(?Agu z^c7$|pg&}&K~Y$CuTQ+bEj+-xrP$3k`)N>L7#I3qhzR#-+>kXeyFYw5wY;c!H|U>0 z|23H(itKl-Qyz);GYsT|JJqE>X3#&cil>eX>qegbA`@-u^>6>j;+Y`o^T;4KsOKqg zu(5!a%?`X@HsyRQ0{-*oWg`seXEA9C4B1t*FR<{~%B^yOv8e{Bkq}DeXgo2Kx%LY1 zQzLC|%Q}e91IMgN*FJA7yK^=4_keyni8eN-_T~)bPu6cOHRcjB2RX0&Q`J+nrW=f#( ze|Vqau7lr?L4Hxy%yvt|YqcKKxcy{nILc=tUZ~JXT`!F)o|+7!e0whKVl*AwLM&&^ zNv3rDkMCrYa_aocfHLqu#&I4V!QSlgfIpu|qIukyUx2y%&SeIbpB8Q)z7Dn(Cm|h^ zVe(b(Z7cZies+v~$HWvDy$|0H^X4Pft;jQsgqoZz$lpL8JFH|I zgKN%u2t^&VJ|Z_sdj43ST*Fa~WN|)Usn$v+yjXqNyrzA2MF`QAT)3A%BdjaQIj0}K zEvmlP;ep27j8LdTu6T{^OTD^0eI83|4`p)$ZCgb{^`yBA|IESsQ#~sd;KglhOO7WK zzf_I0xm5u1(zwBv&L7MxT*cYu4KWDsr|y@gzv;PiXU-MbSHMe1>U8`;tMzqFS()v0 z`1hfC_(Qi#^i57RsUdzDTdBp0Nun9t`)YMBNACkRjt%YF0WI2+jE|0hUkIXVTeqp_ zZ#Bs~t`{KY&p)1gBqMS7+FT|<1pQQ~A72B-J3`Uf(W1vOz{eV)i~TnzcqQT+2wjN| z&dmPBTLhk|yLWlC9`EE;fWN3cHrMkU;%VRuNSfJEa@|jF>{I~!7UrEh)30b~_4HUM zt%B#z7{4gY%^!bLWbGIviR?$j6@Lb3`L;GY+HTp7`Y-NlZ{qfRuH4~ub}x#rsSik8 zznz1mRIk^HP`??+;&KC>b@cV0HJwXF{U;H6^aKsAqjrJ4$`bHDBZSZeysYefhKc$g zYOuechwnrtdt{~=^lveQ`#C_hXyI6qhniV@xqZsk8&}MHf%(ii_MZRFKZp92L7)Ru zYh#zsM%vnrkz3l!=Q@JU?{)V+tS?6hWp2l!iPM<>Ko-q$|W|S3T=qQquX&RvYvrBkOA!!-MG(Ya%k5*Ay7!+v0!w zfrT%;T^V!~**DDfZL>7eGAo>H?ljEW3W3knW-yVn%j+ z>41K3YL60}2Wnz3HfC{=#K&HC-!DLXBB3|vDJpd<3_p~~0^_`ddKGW}1FbW=3(0ODo9&zZ706u*rP z$0edc-*Q?gUf<^Z*X(Z+*&7 zz?)iqyZ<6^_oNrg=d3T%$68o#>E;gcZ!h*>FO7B~LwASH@i4TX><6I7HSe0hxA&ZG zUYgNR6gZL~0DK{^UuiMg521_2reUiRjk8yt$FCdl-qb>_86SUi=H=x3MF=m#_Pt>2 zBEKy&VN}f{z5-Th-w|jxs8{~&%x^_r-WDE?J&q{;dl6Wv2mWe=?Ml;i&R5Xsy>67A zqxTVO!-hgi%FFk7k!$99XXy=KFBm9(X@W2u-lw1KPjR*D+CbyqSZwMG^S@WyS$VeXS@OoRwZwXggxK`XR!e(swY5q>VaP-Q6AZUJCLB zz&CyH8dGmc_OhSUhLQcLHGEF$()ulW@#e6wfAf9jO$Eh{7ucJ}54XcSGO9(4Jzqpl z;AP&KL-wT}@Ups|4Yxj;Ce(o*-Vjy#M{qE|qdf5Qun_FSF!e#QL z{!Wsn73a>X;MsIiW`CZ=onq9lC!z=jEbJhDon<>U%+HQ+YJAf_%E5lj2n&HS=q5Mus)ZjIkLPYv=f(4Ru0en0$eZH64^29mi#;sXze?|h{)7dRb15F; zo$8Z^5~~u zGM8VQMD{GAlEf@Y$CdN9I>ULg5*YY1pC0$Sa5!lB)=;<)!mEN$B@!~}bs_#W$30eC!7`v?2gj51w^cLbXD9&st%`1NCJllKG z?2gVF3ep4KOPqrFp}-Hlwn#!*IK)aiPEo}9oGAC@<(_{fq+9P+e)kKC2MBI{5TFj- zSI!^GZk9+g?r$Fn=EMB?$d8qfFQIuBC3JP-kb=bKz6GT3K*-LWb}sFI;rntnu4o&T zee1?cchTIrx-Frj11S&xhZijy8~Xmj8l4wHd=lN!dE?a`6F1Kwy+4AR{4~${_5Oh8 z;feq1r!bsyfsHRgFFh^hzmSyx?-JEsn&uUu{%!(3sb%`C=S0{kBCI#~y9hEfJvnXS zo~qGz&;vAgB^UfVkL+hDet0&!2f}mdp^sOW2rk3vT{l)+DHlKy>Q+b}iLX!~N zn>1YR<^t1-B<6r{*2^2_x%k8bf4PpCZ?1^!xzNH1%WL%9dfP!~81>V#3kA73or8MI zzrNHnJ@PFR^;fusZ21yY93qAAr6?aY25(jRo`T<4*1%^Tf_b>s)M%fL0+&S!D4sU( z=jdu{u6aK2c11MI8!>Tl{IVJF{F-aiyruuzLmLjSZg9KN!sio>UZgdMZznfvU)mYp z84LB_rO?0>GMZ1Wl=U6{g!l=f=E;XedA-|Z3v)EY>&I%@o6l&5d5v|^wux~6Sxycb zoFUTCR-7-=i=@O!TFtpNH;8_6PQyHonmlIFd*Jh)>#AswhInlVbCbmV?fDP4@>9=M z@K0?Z9V#*q2HW>#WGQ+fe}zT#O|-mU zXV59Am$HTZG;#|FBr|`%s=6!eHP}zSfseL^gYnIq5|U3QflrN9Mef@@-Hf0S^2?yTXCdVZ;rT$uf=?$m-s3?3B@}EvbH^+|LaDHffp?32OOu; z&3--PN&2gV#pyu(F%(k$JX>3KZ|bRt0l){wL4P$tR+hCr)XNY2f%E66Q0;RXty{11 zTq9(2UiM<+>@#kadd0qejTpP@E3wm8XHxov&m>oZf@NQ~)ON*V|C6m*tGv#pqXM`l{lYvA@ zM%wrbSFP7CqzRZB<#Rxb}cmNnXupGqA9ybtPU zTV;RCuK7M->YfUC1MpWPkD5wKu4?ekHeDj#&qAk$upBv06q_V-*HD~Lh_qcV%WCPyVz?s-%0{(J@5NT({Wqf1QEn?n> z^WR6vfQDV#Vb50qBXIrzkBkY`kV-R+Y2O@i0=+NHm!fwHm#yzhq;^c4eJ;RCg@!9| z&k`a6$5=2Asc)05Ggt*tb>Zd*IKX$=q6fIVV#=v|f})KP6F1O&+NV<{OAjlx1BD3b z{Se{;E-g~Yte^bY3!4nT|55>8Jb(P&ahZ%fNGr=%dQ{S_-;p6feb%eLW`o>y@tkC+lMt&{& zdn$2@Lzi%pb^42!7Tl*1=%=rk?tW7;xvp9P?hnjsD9kIaQ>$hTCrksMH1SE_M{EJZ zs7?RuPPFVx=R7!NkpB$}qcnEYMz>?`g1PRJ`yo45p0wWh8S*t94i;nMKY8+H3GI#G z9?F+$X?=$r$(zRfGQ8AL|0vs8fY05$>Myk$y4lff*;B@#7YiUs_%L+rnvI|Vjr5UMD0)kn~uBMO~1B)2FTrfG=r@?nDCCUR@*mnAiu)}4m?G{@G* z!-I`ChF#-YD|dpckf4;pmZF%C{Z*3jEmdx%;okNqL>w-M+?WP<nGfsrDj)TJw3i`u2>+NsRPD2WjmZlnF65eNqovE(Ts(pD7J60`;>K*I{+AWThzPrr+$Ka2F zkL9DLr~hzKb26!ELf(t5p$E zeHpXBPkH?Q#_X`>%tbLj@M`i!dcW#5{`I%rXkIt=Wl@lmo1WfYTQZ!-12AvsD6PD+ zkV{X4ASjSK4l!;dhQASg6WB`SyRAIcb-_(Z0lAKE7Kd!$}6G8gFfd1d5F3Q z;@=^5fs=-M*8^&>*?m7`KWkk(iZywRGPR@eBj~=cuaKH(4hN*IGiiJjKNz_cFzB5d z3j!^X-X7>n7SgS_!&W$D-MJx|eF59;Iy1!ejjn<69k5?8kC>!EQ~#Bly5;0h8Q>?_ z)&?^9&f@!0ounUd|9aUkom%jWmkVdUu|iS)PYBZ^JA33PF7)2Zg!3a1`4o6~v8c^&4DJJ?7jp`7=;*XG4e}bvhWv+{i7_H@j%1OYo&5oLUk)K%(Eq!l)f3r( zv+}zKVrw`%OoLp9j5VAw5Kh7fZOtNrQzv(TPpbH_wHT^msF{tJwaz zQaU`pkIhTI9HX$RBI&r1K#Zqd(cYnF?Yyn5Lq8beS>xbLpw-u z*}J!?T`Wr(iL0;Z%mYv7u%9NIa~2vP9-R?Z*l4DwD_A9d`mqz?I}l%u?TAm^9cl9X z2&~^Qq2-&kmFvsQR+BkN^uD!OT10O0;rliQmv&7g8H)9vMhsWf8=ri50`dWvzZ1l{ zd0DkUv7#ag>S@r=0eZ>&4!xe~gUKi!rTEF`{OI1NG5y|#)NYG@kK^IsU{xmNF(D<^ z>q(=NxQu|UcW{i$u%FHKu1O{1V^8BE{+J0u_yJ)zNrV2Td1L>(^%~%hprfDpr-nl8 z{e2eUSK8rzP^;7)RpLYK)`h&*ZoEInLHeN*dPjC<{cO7p@$3*rq)8MpBvR(|tFI&b zMD>YL*`jlxkX1>*(EXpgqRm~AENk-2EGx9#5AIu>7Wb*v7xl=GpO1lmh5m__&K7#s z!A@<^V+DI0k-q#Sg+Op<{nKnZ^$HSo(oIh59Us2m*VB*19J!TSvsV zs4OBrT6(NApOd{E=Ft)K)Et0B8I#nqp%tFTk9gU_<)(J4nM!P%M)h8TKEATT#r+!3 z{#!eGe*KA5`Ov@o%8E<8u|M_Z>^*dUM43s;-AJ9ZikmG* zQ-X*y|IY8Z*-*T9O$5ZtBUot3;Gqh)(#p+EIFuh4hlX-;>5sQoUSz?2u$nRQ^9v3> zzHUwG-#>6=P~X75kko0po@GnkoDD|t7Z&OydaYY@um70Ub%bBnM@;rBU3+JIqr{OY zGn?8`P_l-H@UUY83EPTp`WXb;3I8M#@;3fMsyaCUPG-DU5lzOd-upXR>6+p zUwnLrJ)l1%u5@b85eeu|8^TmWH!;W=+m|0#hkOF;S14VB^VF;?Kvgsp+e|M@(&cH%C!?!agC*>K2?J^bE+RLVVs&&FX8p7^_;HcD!{H@(&cc7H6Kn z=%AeDVazirdxE%krYsqW!G( z+0RJ8zcOiZ&szffBdAXgowClaf;NA79HR$M@HD zli)8on0k_)wfPm=?GA%9#FvclDZ$TB7OO}sS@zCX)x*TSV^;kaNn!E{gd6+XLn1z#jb{QP&<1b^g6kNrc_DbT0{E7*;NoGE~a2 z-*2X z+HGb&pZEKm=RD^*&jCMLe#eadyU)I0fgz>#XU%;F)g{;L@tGRKTNQ^N~N><8Kn|h->9Z<0WTbg(9+jaSfG=jJ}QTPC%vq|;lpoF?PMz8 z{f%QiFV2>)r0boO`(X;_0nTSvYiqzGlUv`&8Yq5N!}-e;`@Xbf182Fk+XE9dDf(6K_J-Z*l{CS}9x=$iJP2?Uhciwf^ zIHjcitWbFQ+0!q_^icny9%~_HLI?HJCtix^zEv)xYxs}rYA;abbYNa~UwGS$)8GSI zMWpu6`iY~5H?{XQ$9rEp=LwjAjGo~EJ)%Hci!fBo-_CnlHk8TL+?Fn zH5z5NE^iF>6`MJMA27lj9e@!L|55GO{yb|*A1%pZov9k=;g92e&m@jFhs7Qe&7-`s$ij7W8IAq6hlc$6=Dq!&(DM^aBvMXD z&&@@>&OE;^Xc6XD#kMR`VBg1q&pNv=qy8v^A&}pN^yhZT7g4{&QY~x{_RBSW_E z@F%$5)nX7Pp?i)0Ib3ueJX!g6(|6(G0OT+MT@o5w$&NBFKXOl!NhK(=7W&QP0L z`~jnk&!%>DOdb*c^i>o1idmSyv<-5)jvwpqFaCF*VtG(|dpr9yon8pf7v|f#0(Yvt z>r9?iR7U6B=#XEQSVtrLeQ778@2U80bP2wUVbjM6q5muu-IpjlZ!caZ^F+*p2sAGh z?oGl=OAU|Rx{K;7=r83t2DH)s(l8qfD3@~Z(6euU3wmo|b$LhcFCzS!Z0$eN5n~Xq z+&2h#qkNtGg2e~nOz77s*IeK25d>vky>jKtzU%F!vJmfkupoR{zd2*su{8U&sNc>x)D^fdd)?Nq zuk4uTCw-j}-#qEs5_FlQ0{OCzAAW;QY}6R6@fH_T2EL`2qp>-fN_}_pQEO@^n)k;% zCH^My+}6=rksmGisnL9i>Sce(_7-t^;raDY$}Q??>W6DohHP}9{+S7Pq0tNE@J_eq zSFf2!1bz7JtdWk@4vIrp)(7e!zK~tZD0BEz(bbhXiSk8wSp4%zl6?I+qq_5mFC(Oq zXxXfFT5WQnP(N^nIWa<7{^-TjVCpjRuGM6gR{+e53ubGsyHgw{a0cLSIxQd{x4ThB z=PX9|WDaC88UhDEKb7+Rh7H1tl#p1uL#WFmo_lyu!W|++KGdHs$d-09n6yE7A~wgO z++e9vZ;e|b)T^*>$JEr+-zX(;*A0lLP_G)QzAY{2D#A1Mk0ZP+JcQTlf9Le(&0oy^ z=QyLNY40v@a(VLDkrIc@g3V z_(jJK)HoVop?5p8uQ+So*b91f2h@)~00zqRo<3Y$ncpdS z>>{JJ2%c{U@U!8+a?Q`wTIP30vWI?Ghx`Kb{?pTY5uU3`ec{HuOeD-6t1d(Mhmb;7 zS0_Fy^7U{R`9&CcZ_9DLeC*a_ck@-~Pec7@v6D3GqR!snZ`ipg_LUd0WLbfWJlIJMqN71{hwcDDk^SqGrq5L z|Bea5%^N%RnRpkfgP#L-&cNV=&d>RuDBn=bSc)o!OA-p^Zv#HV4Py#QjbFcVkM^;B z2SMNa6#V4QqJGlVzQ&8_K5$%^DmjdMsxyoth=2C{@Zj$qQvoYi!+xIckbie?Xy(<; z%q}j@lO-hqZ_gOLW--K~ZRFdyn@H&WRe3abm~PuH-`r?^LR62vstCBd{_@UPo-d zc^7*c?mNu;#N_N{m(~}S53+qhe*}8mh$3^+fSsyVC(>gMQ@pLRWMuREdMs<$pnn1W zvbmAVy=vg65(@kP>}#gIO@O$UQPeeLe(wplQ?5cHNHE zE+bD-yxM1UQc_a#m-Vd@xNoR`hFy0Q{zYx2Guv;@)a!X1c5yI(`G2h1r>_+F72wx% z`R%K3sD6<*obUzxC(L8Xl^$(#*~sJtGvWTsgItokxKVn$-OG|Q`!anXDxh9CepN-u1si{$8aTB%pZ(i>sS*lvzaY%o` zC6ge&a%QS74HSr7IXAIvvyBq)kRx{A-ZxTjE?I5Wx?kER5LVZ^90&d#_U#hQUTW=p5py%`5FYt^ z5$3Hr>@&U>;m1FNUJ3jg>PJU?5?fWh6QzEL_Iu=y*sl;f7svP7+*2`}_$s^bzG%LJ zXz4qeaC6W9<|7Dc=i01oY~b_5s_`pVN1P4#c+DKn`{&S?e$*>hzMF=f27gP?dvH>u z=>U( z(WE-xO={pke1`od`D8<_99MzGU1g|8;JzlmT2uC}^!HZ0N1J}}Q!M4#B z>2K$G3$)DS02M##j&*bNmTyHPxbK3Qa3YO+68g0*l$0p?QU5#gJ*G|dP=9cRVL62y z4D_cRPlpj-LgAW?1n_FN*nZ3fdJ#AdgRnItJgzhO2!mt|I1+Fg5Xpn)TG z|Ag72LTB*b#ugo2rgi7$B41{(TtxeiZ_r8k7^jul!o}%P4MA8iN{NC{pQx4(u(|3ZpallP_|CU5++3R&Ea`; zivb0P%!82|s!=?Jzc1F}tHtW<6&zd9`*q!>#p&!kbduQx{bs{qO2|||Nr}`a`1H=l8(0(UWiG)iT1k)JR(eS_T!aFmNoAa>|Xwd_a|r} zT26LIZ$)vsr8_#D%wD1SWdZyrYVx&5MES?}Wb0m3-%x_A-)f0d>~KtzRfyjv2TO8! zV&4kO);(&2e8}Gyr8?_g)<3jutDP$R`;c!XN*!jn;76Ty2jOW!WmAil%=hF(YRV&o zCpoSpx}MX3ad*SUYN|e(FX_|Qz;*1TC4JB?f_wn*L?+8*jcmN_M0oVltl|@`E;5kc z7q8Mgpnp-vuXeeofiIx$9liIa9`$oD!`KxxD4xt$H9&Y*?{ILC?rLf3=`8S*DE*&**kc3r#MR$Kc*2#bae0=Q&g?JMLiyIn z)0*38@P~w0?9X0=AB=p&+mGqD9Z5{D|KPQnWbD!zB+8F7y0%Udus(@H9o=pt;a(5 z8R+RrM#*aSEuwmnvWFIV^RkPJf8N1FR1aaa_{_kLf@;6J8b8qdN4SCY-P_GzqVhfw z5BYl#3!zJB_T_F}=sy1w@q=`hI^>J_h$YfVm%A%2l~4=481DqrKS!o#a1j6ayTK1$ zBO^sw%z2*k+%v8h^okS>8?l9)H+cn!-y^_0zh`C42EeC)U#@8X%w<}qP$6H>!`R(E2lS~}^?|-ZX`~0@Y*=PMYLDtw4eE$M=Pfj? zZ1qYx-!InYONS8NBRJI_;9lA?&-pXvchFC_pnc6V`|@uujvSxQ*Yjol+3q<#GcMrV z-uAQ<;wki_=s-~4+11pWJD+gR>c9BpxGSda{*U_gL8Xs%*G>xsmnlQK#lY7b0evyQ zp$_+%b|-Nid>-ttHM3(V8{U(=(|LM7s+X#wlwR2FO;R$I(EOGlZP5L1edZv;OXV!u z=m6d|q~}xLd+0CQT)iNr$*B3x8o^D${!-HmN8iEwY@w8~bZ7LDwFs(%P+IT=^-AFfj5oKFG%i!Z1r+cRS& z_WMzGdtcH6{)7}I(g)vea8rPJ3@+!D!6+z13r~FQ3f@D#e;rp9v#^MD_+pD6>JYx4 zPYpfJ1GivYAXOFW4e;X){^3AcQ{odW;GljUB{{Ox;q61_g7+qWc#0`N?V){^gdOSLcu(jN?f! zEjaohp6@!=DYtDsJ$){9aZsSXRcYIPWzp{|myT|oTJq@IvCEz!e0rm{w!yF|?|$vG zdTPTvs8?rcr%ba7ZF4?3K0h6oiuAxsgT!`Rpf?mwl-QzuhsWi_uBD}ESNcYGC%5H{ z)gJ}_G%}5Dc@g>BR5?y*c6Hfqo^dH0gL$TLiX>}W+_5{^{+Y5MY7^@Hi~yuVL7eO zI;G{4huC7Uuk0T3e{OVLaB2zC7K8e05Yu2Lm0mRK^8WTuI_Nne|1Zu267SW<;FOCu((U1^$08spGXh@JaLA zkIOBE{4|RhEH+%f*(=ktvAP-Y-5e)YK%msomNM4I-=Mzf;iQc*<24Ve4?lQihvIMb zjjok=f$9C|@_#H7eE+A{$2(aLR{T1r97J?(e+ly+^G|Q=4>T0b!eI;H{bY5$|bsQsOmbc4d6M`Sty&c6s)oH~ks#v%CQ59kFZ8 zSLt-~1Ye)(1<3bb$U$LSBqeVt+`Ps=y>2*?o55TMf2oihWls)4{7Pl6bxEg|qwx=- zdSdH&`28ud4sv2jNBqQpS!x5%L+CTyyI8-@aHEXUhlfg ztcQIC!f;%N-~Q;4L~cg>a@YqFaY|UW&K(_Dt0Wj={c_ zC2htENFP~={iUx#Fa2@cBGw6=$578xS#iAP>fu#vW%RybzZZCUKF&ONpHL3-h*oY6 zYtg(&b?w=+C_nOrQ4tzcu?7id+onmW{50cKd{`3T;qbw+Y`9OL2RI|GW7Ejh;Pw54 zd}j~)1MyiJ!vnvs`qrs}?rV6S-&cm(n$+lD1_PkKH$x6$EoD2@?eaGG5`yN91YzRz zf-cw>ww*RZCH7&1uNj`=meZ~_36&V)PdJXb8I}ack~=>it$=vaz;O-rw|w%;?qmsB zQyYk96yO8wwN$xmzWZ<84(?{|_BYTxcvT}YJYM9V6l_fq&v(6{W!Vn+4!)n)fNosu z(pJfko_2Wt<5)7k!&K!=Xy9i_Yl-(~BK|oX$r+y}?S0WV_8R6F5BC&>J_0`ivc+VZ zp@@&p!}E-9ZmIb6vAJAyKDo9Op4gu^?)5w%&($ZIrZQ>!5npGVq-+TIsD-@ScY(p! z@vC_%JU0L6W4KwQ(k={T8&(D|2L_d!kL9V%F-VL z(L8viOWR^6J@J@_g`&7KHzx%SF=Z;Tq00V}(E?oF|p)eEp2k_TO4Ykr@ z$i;MTKhlosPuTzVm7=w3%?0V*lc0Adk~+=okzQ^4qyZk`Q;H+rVL;Hf!Iu}t1$UCq9_8!ZWyz6Iw|>d9&AVu!1O5n@ zYD0bNpj#Zfn8C?E1oQLlZ#&_>RyDWXp+Y|I!MvN|<{$ zLFMj5{B28tQ)Kh$^6cn_BH&;4x}++Uu6aP0rLH_ zMJH07hA+!^aCm8u>2bTi?RmhjHXav;`jElJ=%g$e-0&l>N4FB?SzHc>crd{*)iM~YR*RGIXhB0R@%qNE~51BfMLwpIvlQsHP&g9_2;)pe>AH!3{ zr5gXdCp9M0KY@Pcx~f*y)asbt#N+KKUWK|gS$H@8t=Sr9gz8Ox^^UM7=3b^c{2+BJ zgg>ittvdn&Oono*aqxRVe;DKbM^5~bsgt8i)!;nza$?#n>Ry(||B5RUn}oyoex0u8bwtDck;!Wl46g6 zD!J{ub2rZ20soWj*1hLi3=qEC7t7mQTDoZIw`4zif0|lEj*nk8CXlduk9*HS{H_s0 zWqOd#O^z5FYN9?Fg#FKFyk9jj%<~o)z<=9+CJ&2?J-)g%MNB!o4?T}cSKG5MnafJ! z3FTJQXQy_Tw+tcX6iZQXP2+RBcpmIe7BZb z{*RZp%Y6FS=sduDUxzeX;mECQ)tgZN4H90l955NhttH!1lu^DhN@GU8Fx;fd?cX;L z{D2sdHR4`9K5aUM4T?=d{OqHo2^+GW@NZvyz&3&S1^a@?JPBR-`1$+szl+YBi>%s$ z)7@o`dO>#7YS=eBo)SSG{fa46ex=*kK|JzdK6_g4%zXEb?}7MDj;rs10|&Sj#Y|tp zrMXBj|L}(TeL8|h4}y3E`irM3J8#FoSSgrQ1wI7y$=4m*%p3peI;dBx{k;G89l&*C z8aX!fw?g4@^n8V>Tq3PYT!wwtjW5!h1Y7%9aMwc>YpBBLL9pzM#)@n_^V)=gin<(OYE{!nd@4e^m z{3xajcm#x2w=_bI57H+Usu=#!y3B!HbYPL5--WS7EyO=mdEQ#eoBl`Q!67Rwwn@sDCf?&Y`dFzm)gbjZ3pB1poHkq2L$Uue{9s9}Mw- z;d^B1i;SeD2Z>V;;P?L!?2gY@3wW{VTiXUX6i;CP8a-IwmC$o%a{>4-zRqW%eaM1{ z0*e{j?t|bb*@ogbRuks(#9$rxB`Hi2-;41rAE}O_D>jlIY%E=}@f*Y!zLABNx4V{nL5#=L3M%UZR)`I{JKDB!mxw!lo}0U6 zmND*3MtC|>?4$ggPEg6|wl8#;hxsXf^(q0*LknRyQ?Y(Aym;rw=wg(w!kwmC>l^k* znU_)e{=E;D!DYI3KPAP^`y}}P&+k$p_e~$I2I9*gfu@clIo0`<-}S`9eH+2@jMG-d ziWM#U9GvJ(yJU4wyZZ;=Il<@{2KbFIM_6mO#&l8JBlhB(x?E4sU{B9j(9^7N@9yE0 zPZZhmG0xj}5@OH$Ip5h<>*iwHathW5Av_p~<3%9PE6 zg=SqN&`pdjS!Se_uKFo zgNhCC7ldTTX@PEHe(|{=dC*J!SAXYQzSZ`&p6~;F0s9xksJ6;6edY^*ZyGq|4UM=Z z=_UbWLlZdfu>XvmtE$Cq3?Ap>qnG??lBfOIerob3p=iSp@)qN72*q*@C9$tX!*J(X8Yk}lRQfwn9reHXqhQk2+1SCeB6sc zom3_t3%zV<|JEsg<=v8D9q&fi2NZE0dJFu|=(G&#yF-a(Ez2g_pk6FolE0{*lr8ov=#_qh zlkF|5wn*Yx^%|N%euw#o-EIB7-9H=s1`e)(d_KNACra$f@Z*X*LDS+Q{O7~0ClzK_ zdjEYX2JvhAT*S*QF1%%Zi_}o}s*Ego$ee%pU9dnVb_lm3d{<3d)=P|DZr^yr zPV>f)hx2@<=_6+7rgs7gMNK>FMZQCjsoTZ+aNtLu_D_UvfcH1d2^BY);mz)NkROw5 z1K+RmdhTD+v47(|`fKAMK79$ziM%2qxoNGHS@6kKTVoy%cqLc2Oz(fZcM)4|aI)*p zKWpifLBZ6?t%}F~)kFAbDNc}HJ}lUP{3e`cs~j7Myq~Zi?MpWY%^y{V(d=f9%P4yE zy-Kg>jx~6!O{*P)`PmTrjuke3fOp)kOVdZKOPAGz{=2VmPsRjEt1j2N{PYp(vyZT! z9p@-Z*W);F9G5{9o2hmj$ zy#POP#Q4ASZZBbym^%;ioa2I2@vA3ETTAPoml{^+!G3RBNvM|>tB*8ZIEM6;91CW? zMDqKzJ539>fzJZJ%ABmm_GCT39ofn{SjQKEi>a82(5dFR-F;u+zjl%M(Xfw2Sb5vR zNueI*XEqh=_5OxIWnOaN2{55+haU8U56>8d zG;75#UAxsQdNsUXn8$W?_dXz_5_50Kkq@YURrOklZbvy(B(}bF%*(-jpPL<;U!icm zx~zfo{iK_c9Be1EZLnK=N6}3k$dpFcS@c|1w}NkTuqWW37x~)jA)m}#mSTJjM0zM; zs+H|DuW_lE?Zv=+6u;2?C)7u>A7(Z^f2c3|zCmF&AwjQm7AkmQ1-(|Vhzz?_vqqtG z6zbnUy0N$0N?;j0jF{Y0Q?B( zWz}}KcDG6t6%PeNe1v{~Wz!oy1u0>b67mBFzXBf^w`=+u*OL(EkLq*mURO|u(%_*k zf^Z`{8Vm5Jr2h3Ep^xab=-*$6a;eEx&V1bdaaLE4L(M_qS9uvfP zy9{*xv*Pa1KZ?;S?>6NzD=x!4JDJ)L1pOH3Zy9Mdz2x@DJf^uMB0e2Ey#IySGO+~Z zvxRT!rQ#Y+eZTQ@4E#eooVMASc)W3e`DH93Pla^h=WGk=-&MKzHfn0fty)!%{7FDx z;ls3Z@o%{~pz;>t1NbYskl?_KC)@WAW(*C|=dQY~qhlPw{>K<`aV^rLhFg?rs0m-JtZ(o2L-nyB z2TyX3KGwc%Q`{qXzMq7zLnF3`*(h3vG{OCX`X^;{n>tQBd)^PH0`~{}(e!cKGO!hG zeWx|@_1x>6NjYvXul_813d2MGAEcOry#qNFyOpIxgnIrh!R=~n&0=!R^#`@GpjT_5 z9G1{@beg`s;H5ne`564t5i3suXuzz2N7RX6(-xhSn#*e z*ux4_(1(qOhlz`2%88YDryT*l_y}K+YNa0^_p30l-vFK$_?;y;aNE3l4AR&m>7w|Q zB4>dM+PHP%U6fqmIFN((Ow|9_rR(J2vwc}n2R zIA(myt8|C`(o7GI$)A?>NUtXr5OjIU^(6`Q^C&@)4(`)!lJRf&-iRN>@~j!&wsy)y z`!8#uUhO83B-H4F!&}?BpNaA{<`e4geX=e2&PMM=>fu3<50!_0Z^%6Al(VoL%~L>s zuuOw%^~>q$v*k!{D2SOSlbC8M@>5r=5uP$iemA;*`v*z?2KG$;Tx4Blp&A3>Ur=?( z)>melrD|^=fqzrz_gd(WSp&b|tZvz8Z_4l6UVaer8Jrhl8{LPnJdm1qALVDEXVg;r z>A3b~v&t$apBW;(j^uMOYyvj0zg5I*^3kVU>V?%Aub!>NOIurB1nV-|G1Mcb!S^`oKF<-HN!c1-j^)4Te#l;c4hH_j`93wsJJkS zH|gbk5vsrQy6od4-${Ry+6{aU=qGZUiH#12ozKaC`~~_~;P0`@#`&ik^dnHe72=P6 z?oCIAM9Z7zpWo1bzmMxbNuz^V&sZjWzaumILOnh-G~Bx;))RaS_|kD~4^vS{8cI;U9v=!&ot9m<#-r(EN9SrZkAGs$)e+5Ol6Px? zzq9FukOHeN^m$debZP;+ewEnD06&wHUwe>${PTzWbC`Gf5>fxqD>u{Pt-TJ>ZbDAP zf4P)t1PJT43=!FIf52bb(SmDfwxwtBuEYw3&r5xVKe=4~J}&0=&iEX})9u}q!Cl|l`{x}Qtp-^uVa_KZ$M_Jyn77(y`E~fYb+U>J#9QcR#aK0(d-ru?V^!iPzY5Knia$wpkF$@yQbc&4 z;+k`XzVI%0X~+n~FOh#>FB|V#S^ntY3g9DVLSwYNdyZc3t3puROl5;=|T0>yE`$Cv2=h{Y#_ZObZ6CMRQnYk16Um zVqLHgkg|g_xlXF)Y@q+fiL&6f*^96C-TrkD z;iGx(i?%%ujZwCCV&81+A%3i9TH>~+C}`PD_(|1!mEZ&*pN2j z8!0Kb`ZXn-Z;bWG!TX2#pfVO+qo8M9eB)1F5#QySk#C2I-|oq>>O**f6UHUt?9*3Y zD<`9QnHGxc)hQh`JY}rS~gUz{0Q9dIoNMb zB$K$()owj^ApZIeV}=b{4oBmYc|S_wJT$VjhCvSketGz{`e1jx^$3#T-w-N72^;yNORU2ofo4N$xPjF{hTB7O9NZA zA;0X`E;}|1m^GQm$`1Y zCpWsWQT~U1_tK?n|I%u$HU0$tu~v^*>)erFjgn=fp~+WE4FUM?X-_RHuyG1ln6Siy zpJH7&LE`&a?Z1-m40qo$O@Cw9i8#teZ1YmYpY6+|n-jl|r9QGVys$;7!DajCN#LuV z|CM)S<4*LxD1bM7=-*_cAM8q`Ru03yob;=#_9@+56OXM(Kl88N=Dxv6nZi`y(_sGU zs&qsdRXXw1z=93b(*mCyMnmc2qO^kJec&HANC>~$(ByQ!%GW*k6!0HC90ygE@@G;j z+COmSQ2Zx^ux%z>&42M?jN{c{A1|Frvqa~U$a`y!@k2vF9V-9Px3jyAT_T4CR1|)%^wmB@O#<#FNBW%=q41u!#(U?$mmnox2{ND ziug)SUZ$OsYueU@`&?Os4=8Cl!(JLc-pifwe1-TN4EV+NcC*u}5%y0-zpp0ivc*^cAzv|$mM`>X@t(@_c? z=9k2u!XD5S;4w9=|gQ#OQ9gbm>O1j4C*oPr>oswSYxM? zGF{yR=W7HrYsRmwjy4E9+XQ$C`Wd<3?b2?()4>PxW1v5T3H2i^Wo1ib=VF`v;QP)} zXtuvzFpCxideQJgB zB_+k$-WK+I#dq-AE5vBe|MSBSZ4TUYz7^r;J=+WiJCEr|XtwP^{rr80SYN~O!7sIk zqAH^r6$1dxoL@_<}j9SPF$7Pe^WU z7en)~fRAJt8h_^xxv_1jgkEg?^=yp9Qn?LFT~mCO(sv~fIzNW}!Gt&LvCxfPNdL66 zK5*uvu<}gXUb!MvuY`;epYY^$cCxG6P`woHt!3BWnRfd-W7JITz0K28y7_1M)9^V` z*({jf=VoZFzlZb&pG}`^LjA7rd)9^u4ho0f;12(W_;&1G!&HF()!aME!Ux?dzQp5g zjy%*Kms{BieqJnf(f*~KI)6UoOJbleZXvh?J;_|!eCvlL2Kx?E+2`bkLDV<9_vD95 z7a*U-$BZ8iwP@^+wQwhg0>2CKa{Dw%D!0XLb&?n21Lo^p92y#?2Z_>&ULw2^)YZE` zLu=Wi7zUNP6Zw($sW&(I+M4A&m4SbsZ&YEOL1TPOEm-l382H5)9J4)xed< zf!@|C9iIw%2P|U$V53uonJ>wyqw6B-4@Z6YQ7Q6E@S?5MV#KdHSiSGxdg~HV_9n}2 z^~BgJ6{*(`x$cc>1qkmRZDB*c+c``ilEcp)*tmP)efm$x_rsJk*1YmRh8|9(rEWs? zZuoWUtT^3&Ea@KmrGXFOhr4Lm1d!@j&o{2q&;k8?^>sYGi?=lu=KoSreQxC1b~MCk zvXFVN6ZjvD0rQ?bJ7C$byMp_u{k z+34wHnY`gx>59Aip}ujN5jf&o%^B&XD^|^m^f=*R8SU)SgURo-c9)><%h3>Dz1Arn zUtb&TAnIQq&g?Y!nR1n|Je)J(P78zY-%FtTYQ?wzvU^g~Ndxh%;q2UoXYJo7 zw)IEDpBwdpKZf#-!d^D-iM%=L*M&zpa+K919-a#fVxs=w?z38rqqUL7z3j74kL>IQ zKctHRuJyXQ$|HU7ecr%eDp{l5!Rgbdkxx2^-`3EwpC;~$Nx49~1NnLm=F#<19M)Fe zVFqqQ`djL_^EBFj$ek8kw%G~a->16WV#(XC)92e)!SfxV&|k`yImlc2ob%U3^R@fZ zMr_NL?!RlekWE6rw<s0`@Iz+dCL{t`w+t$db# zueq|X>M_!pk7%Dc&Po5A8uj#njpx8$6cS*sYbgt!m;dbe?8&Y&dS-G`Hm@}qo;U5~ zS8mK|MksB0I*Mn)>ymP;#pqJCISNtTT&<5qpW1_vP?j&jZ@(QvY=m3L-* zr4!nlX8zUhT3qwW32mW*==P z&~%%O!1`$E3cx4x6ekUhXA1vpP8%^Wg@5j@wpcHAC4F0Ze=Y8rh#%R}K=W}oy>ZVg zCIo)pEN89ppJqgt%EXN$iC4rPwiyrqBbryXZEc-H^Y1 zeds5-*^rWWUxd|RgLn-TfA%>lk>1|wO$j=Y8Up!~FN_{2k(SQS3%U3<0O9j}mskZ# z(^v0Z>P>qO`Ty51ugg=LsnpD%i)Ef0)^pvr_mZl|$LkI(H-q_3I1e1j_JQpkJ#401 z^@8a9MZc)GapUawX*N$l@mG-SsJkpr`sxq2%9DtX5(KfbqmoS6zF+vBXkQE^WpPgM z^+1yHgY21nWis8f^-SENVEUQW(?A1wUNewSo3t37jrM(th%fEI${HHE#I}QXIplQY z$1u-k+4*OTQ?)mgofh#0A(`}2zkw31O_SE>e!{*QjVYn&?P4|LM-TI59>HbGh3{?& zwL)*f`9FJO{0q$|t|nsb>7gWee8&2(ugDd62AA0J+lG^?PN3f#etopPRPWfH zoBgl&Hd^p|JvGWoE-=3i?w5r71AJ4O+1Y8=Mrq+f)gha^fcNgbvpBH5@b*1Zz2XEr z(tmhxclD8aoN1xA8pj%9X$l*_m1vW@89;TIHN&02v!K1G0 zNMFoRQKj2;ER*NiY;12WfO;Uu!1n9d>)~j2y?q4<`u7f`A4vcAikdTr`UO>HaGx(H zWiPe-9AZMdHIMvG&gq=+#8Mwl=dV5g%yHG1K;+-;vzufH_W|bFEI!uN5%-tiN09!Y zW%NiMZc!+xQKHg6!~Jfl4vVktC>_SdUbCK1tB(Z#*6vQw6GklX3|N4_dpVUkf5|*~ z*WDFjv<>1H_`5fBsB;JB9?1OSE86cE?fv`GD^yy@?`u;M68<+o?iqI{pr;)0HJ^Mq zH-DHhtKQcd|3<`5q%sTG3QyNlx|d&0aA^DQeXUged%tXrGxgko%~0Q=cpDJDMFH`L zoE=>yNpkU?3k91Y-V^3H^*Q)DuJ=7h&1rRX9>}wNyQfJF4_i}j3(-6l#i<#G)9Ah} zY#Zzr^^@xTpWwCB>y~>cC!qT*AX#hi$QCT>s_bj5CDH$Uw;J+TsE2!?eG~q>{pb5Ws z4)!@m>Mj(=Ji346%es*?V!C!i2kbkI*yBL@l)(OE5Z)9tbE*xqeXKLFp%&X=+xBeBh#vOnt-kUoqP({x+2z#{#`_5+Df-+T^_ zk`R-))w`)pVfHtK_x7dITKnH!!9UefCqn%P{R7pqV-dKRYVN}ma6Z9*j!oC#ZmpjG z5Eq(%IEa0^^NgtfNqbr1n^402FCQfwy7lK+AIhf$FB0r4*_7IIzeR}VIWe<_49iB( zQokYnE$Dq3(Ole}7z?LxU!JeHB)mZAwK2>V3Me(XfdXfkr@;sY>6 zEtH<1AB#?Bii5Tv;a{3dtmyou!E!`67dkS3Xm|ZLFU~;xrz+HsnToe4S9J&0P^ZO+&-&S#02^J;)&D&j5h+mIe0KdiJ z<2oB;z5`xik_jFt{|iG0`q?*~D^&=ygKDd9>?PSHqx}oj9o>5&ewNQ+27KL{nVzqu z$Ggi!_|8#UgTz~}J!!XbQX(fTX#ISQ(V#%)XfJnc4%Hvwj?FT0rLSs+NL?zRHy`KZ zPj=9$s(GpR|J|on6~@GE+qU-UbD4r6$e$yGWESu(is{84Yn67*XYRVsSm*vuK)!6e zdc+U$tyQ5T9riSbB9+_#3lwj{ud`UUR{!ngb*$^~6{)1ar~mGUqJXQ=0IEj>9>F|q z{f66fE{tHr4;ba)OKWV_?x8u*K+lK#2UtYgnAqpV)gu<7djEAveP+hF9psJ*$S>e` znH$X0+%~9IbfR$XWEMJB<&b^WZEjD-dgjEO0|Lzo#E z@g2-2^!=1gL-<8_Kc{h~%Y0?EQW@Ys=yx2l3p^XxwktlHn~wgOQh7B_SL2qsDgV$Q z#Lsa;Ue}aErvf4I>uM9U|7&-uOA|es6^#FKK@Q;yf)n1jG_s(@-`(LF@NL0}Z*vGz zd>yoqcU%YRgK$?~NvXm6xT=yKCcNJfj$^X}g^;Pm-);+faV0N&~-V)4|6V8WW+u(C#I~`X9*B|Ff#+Q|XC%z1xJN7kj|p zgKRwX<;zK=Z>Qu%me`zKZ&dtw^e+1Ejoz{>8>Js6pEh|s2mQDfN}3fTN}+Sl%{5+@ z5I=?qH@-7i5el?67g_h{J%M!(?>Y?$=Oc`Nvd%8A_>olUTo`(G43b~x(e!gDeo-v1 z!#@+ndRE>$Y!!UuXO%>M~|mIwx2+6Ca96!|Co*90l}xOkwo6H{NR`2 z2Z-;$q9h|r`gzd>+-Jl0M>DNp-)a){%fi2>nVkgwcNmMx)ZmeRTSeriO+dWi?~C!Z z-$_rn;cDc8gL(;|qpDW*ChO;leR~(3^*j!cIt`Mb|5@GM&(<8?6lXg4>)MM#0anv6 z&A1}U=b_A`XZ_a}{IU<)A%2S!t7*4C8;wwS_A&7XkIuGI<;7gaL(7-=!Y5sY9iz`O}U+nq-ZydvPJM51q zbo@`h1oLzfo6|-0WNZ$*#$iM~qE2Ht!R~+aMUf9@9%?;B^Z%ISVnxK8oF?Pmp1;8&D!u~dc&UX&$A5E@*RE+#aDBc;;wSugDy<0+C6N>+b=OcIZ zg){>G;>_$eGvT&sTSqD1PMPElJ70+|!JHM!tVAhzRv` z5}HR08!X5~_{^xwqA^~gdG|NVdc4#@lCc}lzybV;s$O?IUXA7vDdn}#xkQ;)0q4F{ ziTb-Syx7dZHMq&n;Am0(72`ipT5#%%ykC*JUAkBD=5B9ne0pCiudWEbUkB_r&s0w7 z?|r3wrdEE7YPYTDunT8!-1xG|RfT=#6;W2{GR{B7FwEGZV`lGtW0F-*2M?x(=c~{@ zHIbe`#qNg#?^h1@V+ERLf_>~tyg%H3yUn{#3%$~K*Z=8?5%fAJIcoL<&|{?mzl%w? zTqvw9$O%LBEIF7ZBNG?7;rgcVx1#eA!hfo}ZLr89XHF6DW*dL}&(B&hYC$;lrxX;w zs?56V>KOiOj!EOLC)iv4=jXNM;kP@5yerxds6kCClPL`UkoMXwg zXl5Vc2gBLTOij%Z}tT&MeiG{e0k5IYiY~Lp;k7;yJ51=$t`mqwl>Xl%g&+t zf?^;ip~)-D_U^t3dIHV}=VfMQoV3B;xS1|G=m~dJLq7M#Ls&S)(Jep?)`3JQCFReT}7+o661!fS-x1FlbP+^ZHwd0BLJ(<f`1$TUq0cN zypn=}6vVd@6uwXT2E9JySKZg!st@tRvq7n&`c1*Gg=X{>#|LLC)xTiSPod;d6M9j+ z#VX^aCFzIK$wjZjQNJ1V*mSn>7X$T_zn4v%eL4^Gu&`gjv!!OJO&-NVPMWC=iGp3g z=Qwx*|JqA|WhPlW!&6dkTA1401-y0rRxJJ9yLSQS-}?ezJx;0AB3c>Z%2nb56m<~a z=tHz|W;V#C?RE-;?+b{S{Pm;NKzf+3`-$keTUJ@)-FnDRN(09TPbh2p-##qW(_$M3 zZ4my-dFP{*Rb%NO{5fZF;Gu-nX_A3c2OIqJV86Kkx(%XuB6%p# zP55h5+PQ%R^SQ_D{PQE=Cp~+a#%S`9%D>c4C%UuCa)?>tNJM{~XXx)FYJQVUN%=_U> z?Pj*$F7Bb1q5HtG&?lZe%lS=irS54sPoSS}#xcaC({0vYw}tuLL5h#ckurx)hy54# zuZ8*p^mj^i4BCOD;y~ob0)CD$rc1AiXKbFD?t=QckPG`D7?$?$4z|yN-$oGNFOuU- z9pl4|NBuSLkzP)CX|lgS?9^e6V;5F*|A?wNH35D~n2@5rp}`%^*JF;ys-@M>kOG|~ z4+1{RMfhx6W>7;&A?)|N0OvE+-MvI(tCP~!3le~TVSa!`&t$Dy(bU>J2Ka24kUA2V zua=)5TW~Q7#Yf>i{uFflHHF>2=SA^^8_IW?QsqKadfxi>}`Goq6ZuE^!;++p%QFj<%*d*}enpKF~kbf70QC|Iz{( z*YhO&zVctDZi!AmplGdGxL<@<8VbpU|6_x*&wRHUY`O&B_lIE5{}J`&@ldbt`my6DOj->`VGP+)od`uCgfvo=H7SJ3zReh986k!i8I#>;5ZaLnX)vZS zzx$bU&hP6#ui4)3_wzjWa^2T`-GNmkk$ZA?j0PdztDWQ@!T{k&IQO>PSuVog!&1vI zH8Oj*#tYwYtU!36y@i^3;MT$9pSLvb$&z0TY1Kh|$89*SWr4Q_JPqbW&d$yeUn=9! zJV4MpNb~&?Batln?WPn3^22$~^L-}?RT}#}js|&l9|3!Hk8n{hZ`5pXU1K`>pIB5z z>TMg0KKJs{t>)qc&JXavQai=(0HX=;%ER-9PST*qX{y%HZix7Pr4#(FKegHi^isB{ z7QlXj`Cg=Udw$2(o{f?TXuUZllE7~fqlzn99ZK>n~A)>65edgi?hlO(-4%K2M0*)P(2l=^*7gD)PU)gHI`% z5y@IPcXX8D{;TG@$aS4KO9ypT*~6ZHxVL-ssE~QwUroi8{r3g+6OVTD7`?G4udb0) zBi7zwG#X9`?B{m1PhN#n;;XcKVA?8t94Dd*a6}k)D{a0~6u{m_L!yN$=Ac z)zA2uCnn%`;H2MNA@GJPKT%f$9y5lmXdBf>j@!`P1N*f| zqU|TcMD1auRSd%G>-K2oc`~K9d>lM&u@mJpP85>R(ebBhf5uJ%UlIC`kJ$9!i?+#| z@AF+H76|;Y4K*mgZf73XMc3O9WlxYT`%BR?qZSYQi)E4m(D1^Bf*;HZa_7x00*Q@m>{m z#5X_N(^s_j^M#uz9FuO{$)E4ydqMoT5Jot(#>DN$MK9XUCS?DOT2;1Nj@Wk$oL3YQ z>|d{fe9R-9N-NVs9|8XCUTOU|LZ*T>(CCaAa=#wFo8YA@Nzz^K36D-lF!DonU>7v?>!xLNXJ* zj}y$toN7;%58%JghS8RI;Tw^?pt&En?bCbKbkb*z2Ki?fHoDB-*>8Ky?H8A#FD)k4 z=btYDf5E+?+0NUF&XY-=#`;!-)C;T|p~!#n)1$=GszoN1 z^m^AoyaswDt#cqKhDp|@Q<42>2u&bA+durUboC7>-~&yEx1L($2n%h*4bNxZ555-@~7c3Pglf*r!oFQ-k)Q_|hasZ00gyP5Eu;H$4XPs|dX> zt5Cmh@a`soK5AHMt|SQ)rR8~5))V6IE`B?Gf4S)igMRUf_q^8YU2cSdN8ouk!!?A`$N3`Owy|=vj;OU*r`Y9dO63v4-;;);eHrnK!L=q(#;T;Xg!`QK?uvc5T^U z{5QZCK)*U@(p=|HLw$`F?HO+>hfllFJlnX%B=pybLxT9$uiUYb_#BSSn1*H_q1 zS7)TG-HW+E8TAuMFE8h?R)y8Sy?v<^;q4)p zmP^|HIAiUW#O|M*x^nrN>THt$58YfMHTpApx0iZLsOKKnXX$-<_j$A7CT2W0Cd@m3 zMv5zvg#EXiQbORv`ixxk zXiwD;iso2De!+2~SP%CeSj(>FCa{!llzG}Je_&6~H7VG$FM|C(3;e|(1ss!2PO}$B z`LR(jsYG3~s0+eOdC)U3zF)raBh+iI$dnd*279}JH7Gl78+CW{qZTswL+D?93Gr*h z-L3RC)p)S)gEWj4ogD9y@Mu-SPl(UE_>N2ru5bV5QEi+*#H*k`BhlVo)l)yKL9cIP zCq3UWWA5^U$HR6WbT@(52Z>&jQBkCR&2Jwk#E^atXNajIe>BBb%&q#Uz@O8F866$_ zH?lS9I6=M_RfY+4wbMAcR_QC?MWBB;U^lkvTsigqz)AG}FqpqUzGj~U^OJ)B&p`f2 zjm|K$9rIz_M)t_a`(VqvlVE+e8w4SH#wX+w2yW@h5A!Uk*^f{^GEMIb-I5`4o>YM9 z`9>bzd2yx9e>PiGtN?#A-Vjtq5)yxPKfdM5grNTvr-^CfUQ2B2E`<08^g9W?^bXTb z5qZ%|5D!CtKbfhoUoHRoPQQv7isy-jW@dX^)K}IPtHXJNdaeovZ{_AluM9H`sDS57 zb08@w;Qqk+!@OHTJut{$0W;EleIjP-E9`oQr*WYspD594WLyW?!LFCh*nILOgRrA4BS2FkAQQsrKST?DK4G#{!7QFDt6` zZ3F(56TloS-0)alH*EidZKQwblaoSGarEF|IH15L-`!&Nn2R5?b07Ioeb?G z;_j@g|8E{m3GT~G0N|0!zj-vGqDGWU+sqV9bog@>1;x}yY zz+9jCooY_d2L)u`P1ZDzc|Kcrwt$l8*J-U;x-iB3H!LxaH5}27Z z_#gDw&WPAK5R1q|>9;t5M+`P%YzS;--lhjQ$x;Cyyl?=&D{_?T$Nr&!@Q;Q9Eqc49 zkwNG>!<>KcQfUx~{XXp59}9RB*ozn&Jb_s*_kh0A5#p>j%~P$@FQaM@+3L$b?X3e zlf0+yEgOe`jdx-H8GZJedt9vIYU9?Mg6g{sWj&A1?L_@c-Z)FEnsFo6vm@_-PsQgy zruW1O9erG!l;wl)E~CQtSA~W4X-*EAr^3AE$9Lxz;$VJq1IPOG|9~GdhK;3UTQF9C zqiNP^?nHQIOb&UP?|duqk&-349)6l_OUps&iUS`Gal3y2{#xS0C~0en56L_;jLvJ= z!6RMzm(5#GZyDIdYKykY^gU*R?)M9)Ru=62LW8%DpAEiCPU~a^1>Ju`(f1!D`E5HW ztTA{V4hvy=y_8gYiOQSUm$rg_jC+2vcJ^;w1KLxCKu@=N&;QSt`kWAdmm5OIlSNoJ z(!vH)1$cOoM6vA@|N4;=1%b%^g%{BG! z6K1HBeaFXW4Xfe$(0n4b63o8eR^0tKjPP#`i6W%RbFfw(r5=UrT?ot2EG^Bm6hE><>*2l>b9^!4=*Df(z|`Puy-50-Vf86G#pkCVf%x(ZF|}-Y&nlPl!8|=#;7@#Ra1xf0kbhkFK&tCG@?R$RFp8kRS9w6{ zq8Io}cztztPL7>!j-6=->v1$T^VQ*>9guTa_PD|4pN)sqgr0~HE^G-=GWmh$pZGq~qC@-n%8Pq%zG=`xM=X;9k+&Sq zooLT%L8k@y!H)L!9%!u@0NaG@lZk@_$-=Eqx{)qDvU?)2p0oz$YjYQVopnCz)|{d9 z;t3>Bqx_+8l9;l!AbvK~)EVtp(Um#das@qaBPU^7H}d*go6kR_S1CdLq*pM%jmC90 z(p$w!WRSihe2RNzb>$mA#HZ|r_Q_oY;+Y6(^P|2)hHec`sC^VZ-m>Kx6gsAaKh(mw50 zL;jwa3gzV2ftA~h51c{%m==8GO~0MvHvgPt*w291c{_UTSD3rSllNsARKh;B@lq0-5?V2IiyX==b)SIK-IT7B5h`CVuawl96G`zuRqk3Eg7xU zY+otB*PNGL5?_ghD#CteVPk5jpkEk~;4@4^_fwa0C5J@bvU=t>k}%kRm`6Qu5l=s; zlc>d|LcTv893*0)dw;!ZOKj21pPaGJW=Ei8e{c}ZJA=_ zkIph^Q2zkEbQ=ddn;e?eKL6w-jWQ@6Aw6}EZ@Z+|o)-KbPE3f@WL5t$Myi5U9BY{w z7mV;z{*O6>YE%zzh=%&Hp6a#0y>dU%`EQ8U!t)fCPsqG^dbL>BZ;#{M&v^C+e#4r{ z5r{w4Vcr7?`h7*4lfU^4qkK1{wXDLNfn8~IbKSWlJ)4Dp@cu*U53)=A!5#zOx>J4E zuKU@VPQ-P{el@uJvdgcw%k?LFsx%K(d`5U?Ido-c$UBfAUR!AN&Y9*;rat-ltgIjE zh0vc=W;dmGubf>o7KRyHbkC|IH1Maf!-rJ&-!z=+f6p`(>gWmgfWL@!rKx60rgS)_=HLRSZ zi6m8&{X1`EG&FAjtCzO!%>oq?O2(O%qQ+(A0U4o`1#A!mlo=? zeZQdp!l1GnLvJx{x&POaV7~@+T2z}RkiLpiSgU?sr(?bsSpuG7Dk{{pjONQ>Ye^Q& zxf9X@P9{Ow-Glt;qH?DMOdnO;Y;hXpFQ5mTomly{%1GovDzY z!Mr>BP8@E;ZmqKL1z(m@F!j`0)wA<}N4}d{zzgt9n7`uxdjHn8jB2$Cq{j{Oq$~|o ztcI8Wl+hnw@bfGt5XYwD*Kkh`3$bR-nYr~H-UanBE=|i`7}@uRLJJ&ai$(O_gzxo& z`c^tc=zznfxZBScBM_cTE8%cLF$2*nXkNpY)`vgy>?O3*K`8uESW_Z~d{+WN$EbxSjk5FR2L@nht!lGxQPH0!m? z(_E*k@5ZlMMRyZl2f==Z{6|ZR(APfX6TVMo<_>|4UCwLc6VthA$}cr{AMi1C>@5sH z^ERLf zcv?-|`J)*Hrg@J!!2I%fLa6d6a@MO z?d=xVhId^Se$YIF$IbI^{XOYq{Fg>`+iC{^K4tBeuGI-T2tBAr#)vB6|Cf6B3%S6{kC+1ok5klbxMCZ8VyDiwX9Bz7g_uj7+TT&HZ0L z9#XtP@UOt@o;a< zbx(L+3k?PHz17Bcn@f*>J4Hi=D#SuT;C{0DZO6Gv}x3vvWkA!|0FTaq!_WT1F_Y073IItV2 zz4*Au{?yaf)5$3QFj1c*)0Sg(g{E7BE-AV0cD5Tpe4?g(+!_tlnafRulu7SV=>Jty z4ZtQ<9sHL++P{MG#2N4hz_(LX6x5y-O(zfMuYmJ0Xkuxz_l1~i!W(OoSh(InVj-#B zu4zc?wxh84#Ahu1^HI}8lINcOKL;Zz5cD|59u@qjOe6Y!t( zP=3#iX{pk;PfjL2P*H;QhIvo9q~emc%?Yo9yimT#2~v^N3*Y~+S&r)74fA{8`AW1O zU$awK<^sfbfIq(MWb{eh-Hfx120m{Uo#k0a=gxtO?i^k)FA4E!a;^DGrKO}}FD@|h&L0by zaII&CdMMtKS5cYpAfX;%3u(g@$h zhMGj?koK*nehw1!BSV82S!Im^_R)qAZ&dH1wYgSSN9=s2?t4K6+0(G1L=sRfH(hzO z67MOMS^4FEB$R)cdd*IFf1PHb ziRbl+Q+`YM2K@%pjmO@<3%OLUDCsTlf#Pq&dRGEPpGw&;H0i@(;;WdQZksANfqf|8QPzbTV2*M+b00 zuQxO+XiYB7@%bL&yF6O?T~M!dW&{64M4up6d;44O8nmD5ELyk@gck$0S^ZYOX=|Z) zv6%Eq7`1B)Wp|D8$7pBNI`g(m{`?em=)ZY(3PTr&y$(zQ(nrH?=*%PcN$dsx3;7!8 z9n!-dK?L}rDog3%g@dCqDRe1|Z7tH>xpGMxVF?#6_`xWL7 zMV9u5IzC{po9=+~G6D4`k{$C~N#ME5WVTA>|*u=iMdXSK$ULoWdc$55wT-x2#j(^WMt)@wKVzl_tJ5p!#2Uh!s;` z_vU(;CUq~s8>|Su*9GT8^n#ui;K`uxI-qz>eAOAgWvdU=!@96eB`ccj`VM(%Jlkx2 z0hoGfIjS-uu}PzvwLZXy0KUJlJ&z!)CRFIdh4W_SMkoM(#HU^TNke)JT|7)d{nGNv z3+m@y-rke<`0Ba(m|sm?+OIJx&xp0&$BNJczYqK<@{m6Ke)&rxp_5L3v8M9&H20r; z-F_K|V{j##R&9z0ygy)qb|xe;modc++2e)IJKvtvJ3x@%*6Y+rf&B{lXF+omVbT%PXQ znA@AQL;sn7-sdDMay_xm$SWD}he@JCYxlAIC%iKAKLEuBtSx(rjz>f&2ZzrjgUNEY+VG>MDt$e2*Wb)#pMl$y)M0 zzku*{qu6iej;^?5`3Z(E@+XaL1wT9O&8VYq3at=7r*V9FD(C8!oRp#a-vs+DO|i7! z>Fq7HE!}79@tTnLLFvn&S8-h`X4@au@VvP0xox{Bg<;y)HhMw5=w&Hc)e_~`V!Pbf zXub&VlF6%nFXzz{hkr%GR@7c_cA1*8)mr7uYs~-vY#D$|1 z+sUo1QGe_0xd-tw0dF9vA38rN)2IOc0d0o`V{#72tXB`E3Gm#)GTXjC4yjd$tYM?| z;}&!h)(BZpPkam#_uPCEoAEO+AL?1WW}ZPG(sRM?XrDVv!RNa^~{bvGjZy5i;q*AvcFEo0blBhnkAEn{A&mf^x|qGt~rog>I3=d{&}OeT+q{#)h@xUQ1ZyXQTVYau&IlCPJ2FMVGZb8t%CkDzOaKW zzn8*X%GMj@6ZxV+YIp*VQ{=T7@;Bg!=*K4Zgf4`Rg@!u_@P(lEJgN1oGGFy|qIh#WB&;!30#gx3 zdt_22n2(nwN*|W8lW*=G2}bd6NNVT*cH#MMUKYu~C&GijqLdowl#6Tjxp%_R5ygifkiK7VpDdCOtnjT6bt0?Ob|biEM_ zTmOxAf%H@O)@xuNnu@%*c3^*p_|NV87_o^1H$VT7xTg_l2KpLypoiY@!&F8D@tYeD zB-E*tQ>7!sR@;&Gaq7xe$j7{xP8%UGZ#xgU`Ds4QudI>^{=SV?2znZBFi+W$ejD)G@vs+) zGBOfdKkjw-2>2WDm(mpJbR458A@hK9QuTj)&FngSfn6Djw;Dn;`;3jBJ`DW&JOSqK z;8HZ`c*q~a$qr0oRG-HlmLOFRecZOp;@t&&f09^#)q=6>br;j+)m4Odh3m_d+B;Zo zu$_Ak{tx(DMbR3ycG~|7l53nr__(3hr`{*}qnmu4b;VeVxlM&yf_U=E)#C63Sg(n2 zb>ZlD?*<<$(48_fedP0{&jr1nrQ8I)h2q zO~|N&d=U86wVgO}w#$Qw2yq<5Lt{jorv97W&b>O+!^d>ZuvUFilt8MQvWbA zw{T^uWX2!q9oOOXVE)UDbK({{+;79{@UW}zZfr|5y8Ent0>ys~=F+&6qQV>Fzj>^i zIjhg%m*2=Sn>}rHytKtqHT$CFHs8mBdD%nwG5u@?sxJ)Nl;LK3)sGLTBRr$ACi1GV z_-Lp9`Xp79zlEo$C@{2)y(DBWEp7JH^Z%$9^lO>lbCl1AdRs5xrH8*iRU~DsdGz2i zR)B|vlqk}dC7yg_bQU1I3Hl2OJbufH@T8dZ-Js`ETn>5#a9;fI0Xn;sp?OhQ zgCDjMYxmwg*SrM&4EXA8`*LzhEH}}GWKSUcigvHD6cb^Wz;w*w2;%FB!0ja$t)w2w z#|0ri4l#J2SkxcKvz7bWoZeKCz8d~zQ$;HS$o6hUufa+J=auxK28As0(^s5 z>BzP>$7Nr!u;&HO+mSF*S@Heo5tp4_5FZXrDDF5N7n<=L?ZFPQ> zWiN^1!LT6j?V3zk!?ty=T3E%A_1Rh^nD4{AV#DMK7yr6TbF$ui8Rn_{>Ze_R{1Wt{ zy_iGYZ{HGqM_O^a7h}svlnD!@->t@A+uvL7YPR8sEa-Rgcpxe!CFqaaVINZW$ZFf_ zbr*)a*DnKKiXv70;~{PFD(w~W4<;6YegTAQTj?3aUckSGe)suV8}%`JYSUq1YseQG zV)X`l+n*cPe_pg`e)CAsFBKTtUi!;HTLjgUvFQ{Ms=bYL&xr(h{;$#4_QPlP2ZT<51(2#I^vie@1hD z#%|VdzOi1)UUPQEAu5)#`Q7lmX@^8?@eE1n9(m1qG_SxBFIxk86m>^>s!Mkw{MNo~ znX8&j;{>>7qU+~+1^Q7m+N%{zT53>!!=(b>PijEL>*gz-0KarhXeLljWcip4MZxb) zW8Jl$=;|%pGMBTpfO+1&-k*Hn{0V3C7S;&nLpNY}nRefMYiHuv!>$^cT5b%L5s`rN0{Uz1kfGRR-85f+5bZy# zM_GmShiCSul@IMe`5iI3%p!2>N4q28`z+ ze=wiql%alfqmYtczruf~#bi_n!}%W~)@U)Eik)uNIk{V+_zPOlA* z@vsWHWY>nZ3F>vrNoDne|I~-JHZrGhauDB+@sDXnwiB|Xhc?Cl9{~FGy?$RWzJ9*p zwdZQkZ^VM$G{Z3(8b0cZt*ueM92Rtho+B9-*S&Yk38ZHc^0^EXUphvm`7-YM zdEE8#_o=?=F_{xWz^{A_{C`_JmyJHd)afw+9+cWc#M5 z;e9fK^`zPpi0f6hQ8({bNP5)sZ#^RN@u*fUuKC{o1oM0dfd#*Qb@5m3z3>0&IUn$Z zQlgO1zJ0+czoZ#mjmXX`(rjW$)&X8W&3h?);?l&LtA9Esblfh3 z^A32DoTg=m^kC)_vp;=+&p#I4x;!86LM|JNUx6r{HWNY!sSU*kTtQzpr3(B#4d*N)1AHEse_H0^IN5_q zczEe7#2Xwey~Cwx#a3!tkJ0c(RsMz71h ztVN5i4%i={M@!1B9N3;&vr&3B>FBx;dzkDRfB`79jA7BA`XM%e zr`K!D)}**p3cyHwyx735Drq`i6SALJVE z>`@T-f0!RyEh}T%jBBEFABFggWucJ zu;_7pt%6I^r@{3=e?gf#Ia?_!RpHSI3nT>R7Am1fM^I;&aCbb&ieT z5BCqzyi1DT3+=fPWTW<&Wq0GCTi;8-2l!1|bb3y6zWOB6C)oL`HLLah9q_l%zb31L zAH-jMPA%gNBYi%iis>HHOE(WB2OB~D0(`zbCAohw?`X32Yt9Jl`(X^-g>bu^bCx!b z&X-B7o|B#CJGp?L{{C=1z=tT*z;gytyZ22$62zl)o-#NJTMq2p2tnad_(h(FgbWQC`h+KD7Ib@;&b z4iak;D|>(ZwrOzP`C-J@FsUmr^CL*-$A1j6fcRp9pH^mWmUDaCfx=UGbbUsc4wA85 z@}>vJ)T0IKnW9o-7ks9H?VONq4g6R>(jO;m*y($gK8E}^(IJ<#r~W1;h&&Ab5BN9Z zoq*3KlMdLdm_T?N4Tqa#B+}Y44T8kUpXOhnuROT8fFT4{Qn|13m`vOb)5iKdoA6%iApUy!k-`uA(HBjvqtu zJt!ZCd6wY>O|_Rtc^8g=|AhHH%s#6qbL`BE(0sUmI^Tj|W7!e7DWlz}KjK#0+NRx~ zz{2tQie^2;HswHqYtIJcuNxwZXP=<@FVFoc@SAMcoX-Cog!XS(2%Aun{Yu|zBp2dS z(2p^Ql~AYrzShh6?1FVQ#4j-KRguozZClEL*I%;3f;uhp6w*{~z9%C7U>DX~tesH4 zs_tCt{B?-WApT=IJxRT}l)Nteg84*Zcait8^)R3NvL!Qc3*axnm!Xv8h)kp=S2h4Y z2k8`w_=iEV4&Lu~@tbmWZ0kPQ5AAvwjj6Fi82px*<}+Oz5uaE= zu^_a|DO6G9Z2{rbVc({)>F)t59;bx<@fY@t4V1J9n{J7p==+E}=3nJAq~BWfsV2_c zg?W>=L|Z1UQx)H}Ydrz``jVIOp2p{sa*pSB3?Y6_1L(DA)!X;3aHzy+WrgIxygMF^ ziw(>C-&tdXM}#@n=<4gv6Ysnc5d!-;UA*9sPI8)^BG}?B1$>KOj6}75+VQVZCWBk$ zI5{vMrE!e=h)7&m&_?=dT+qKIYl=NjI9E3W&wG;gsi3Oijdf;S2AK`jx0l(=o@A4Zi}rfE?0y6VgdN_;j-wwggcg04)oun%?@uljrew0 zI>j!$#DiRO;uagOmu*sKHhE01WvkV_YB8|ipg-Q8x65JMUY+X0R|I;`mt3n!5nt^w zZf})$@@X8t3e~RfLR?1Mflv<=FE$3x_r^xvzyFmkVPzc;_CKa%yRzmsZ66CA@Yldk z+oOl`@{4k*Au(!E{J<~Pd!)*c?j1W<>nF%x+6Ha=dbj*cTbpul<6;K0Nb4T($9dtt z3>$p6KAZ=0>0I-rHyPhHQoIHI-iP+>VlK!v_MTd^l>HpBJ;n@uq0{sB7Y99(XJBx^s2cS4nG%4CGFnV$>Qz<+l^{iyOT%riW%Veu90 z$3i29Qcm9UedEAj+z6`IV54mDeRDAf5|xG_UektrfA-lk!dxTgd#D2PZzgF+O8O`+ zDAd!+8Ne=nN^c)ZVI6$>G9kfj><@=v77P%m}YY z%Y^uPl2#;RYsZYydLCN{@h1)R30n^wEq|(0`uEx7iH*)p_w1sd@TPu+J?9+P1^kRp zGx*X#nG(*`Q5jh(p2&Cx`l4|tA2knvdfpw}zwxH|^}mKxzd7AA$jA%vc|u#b`X}pI z=ikUb(Tc@#N_4hVoO20A`3LB0Ti{I$==nYuIS`-hYUeY-e(H_t_?~8hJ%D<>;b3>J zWD$0`nHnX)SMDi{P|ModX(Lphok&k!Uy-=LckJqrGZ{emg^`8di54@lvh1s}=fEB> z@B;lS;uDXr0g&Q6?7u~tzKU8%!2Z?!Yq6ck-y1nizMJ^6qU`POrBMHc=MgQVUhQ&6 zj%_XdXeaP(LSD4%)>a?4_q;J51AK&e=tu7!m9?*a^>(Bd>MzsSOO%N@_FRY-{_YBM zsXH%7=iAOM!Muf-)toT!hpa|R%G3w)I~W8uT*6|-R=)k8m*9EPKa9k~{+|!0&$?nq z%twN^6j?z&#UiGCH>a0utB{_5D~<4WT5xoPZa`+t;O+CgRl2|r3V?hP`j0vW!wlNg zh&5S$D~EZ!%d~d6ULLadM19Z~Vz#LID&m#}`>_?~FK|X`4L{no!TnFr*g%i495k-1 zWHeR4{v*;ybTwYZ>j)U|@J%ocX4)NIq#|4K?L32I0@!Y($2*Y@- zCeBBAiBFtlmkOcv1ah>lkVF|r=lcmQfjvoAuu;!V-w7QmvzxLy;QPNiEU0@F^b&ZkPMhUm{U@*nPt7bXWBRGQ>3MiRAbr1! z9VQ%$^ZVehCFrknkSYCZ_~hzJM{ad?<467&e>}>zZ;zfZA*I}0sAQMC*5p&r2Mzx= z^k5C(4`W!&EW?WZCe>5wXE6BtNvyevQ+26tTFpe`5Q?8@vi1~N>I%@%A;RmRUs8L- zT)ia0_x$|&(-2R<{<+exrTOfBBX63jwW-Z&xXqn6J-zGvR^#TCf_PM1q)jKF<^Dvh zaSYdW=oU{zQuE7;N^SR}y{=j#LZNK1jf>QShzYhFAhiI>ut;&>M zeft=N9l?lOTW=`v=R}eP+ieDK1$>VwC*$?v4=&3Wov%Q6Y-~bYJVx3gR;wpCyZjU1 zjejff2=3qphqIJR*1scs(N^rReVeM4Hm6}HI&V>BiR7t2x^A;itOS1n^B_sqbh9W- zL8*nCz@OB8;|%FW#05_MMD`Qv*+&9RWHPiLR<*+Q9|r&DINbeosCC8G3o8-7xzS$h zP4#POEmpf4UUe?^srwq-Wz_$X-9Fw9&w~YeTt}_)STPwolL4?_CTNau0BG)`+QIS?)B&Q9LKAw&%^#$NhiYV zz-K5H^Q*~?#!ze9Al|KF@mqo4gdg1J|81BLc+^5eP-~azfJkqIQXMfV=|8=Q(x+yU zSQ@HdVC&47RE^US=Bg!Gk752;VYy)`)SoWP_szK$?{IzM+1*N>Utl-Q$};OgpXpzI zVA;3<^~51Teak+v+nn)xR>v+Au-|aLW8MG88Lezh=<>V{`(fV1i&ENzA;mnv)&6yC zG8van*_49t@PRpn-6;MHBhi@-4slu?vFeYrYmuJtu;x=nU!Prp9pozz4`!IyTB!dy z7XAIiuAP9dacab4EncSx4rDkllf*q&v#*N0-$c#csEzo*2p{Sl+G-%NO~)7F?+fhL+sj6%u@a^BIX2M$ zz%0;)eo=@&+U9%$yQ%R0;7+o%d~X8tq+tfqH{-Jg%|)35S-)aTSgRoZhxvHt5P!LQ zaHk8x51}8L3g8|Hjpo96c*zb?C;0hwyl%D1`zeX=Hk!CCA&_l@OHOWE0{f}6P_)o8 zV4I_G_M)yb$}ed-%F!1X2 zTW9xfRfsQ3a%S}6JSaUj3CvbZ#`KIHH8k(+$RNMU6-s^%HPsVfU*LR(by#Ui!7wdPNm_Ol(l#SLWGdnq zpX$zg!+wPGD@-7~j!V1Lyy-ElALI+-6}_%~(eXnQS@3=S3uB@KWmj{SnRgHFMenD1 zClq6Rd{$=`uYL&m-z4@B^@5rDvjO+zUIWx0(0E3~!X?%I>FMVh53&v3!+CjaM%v{Z zKq$zc-Ys>9N)pru7Ou4Th3`T0l=xm0Z4FXuQ)10 z8R3h||rh#;GeND&M-iKA6Q8c2EAN=bjv0dW-=2b`)&tw{?vubDfbcTGe3P@_8|Em6Nd(!ZA=X|UfVc04Vc5%edQDD0r= zx{Q0(*sT-fuaEuIaO}+wI3B;r-xSP)e5t<7DWP!Hl>FO$-6_+9Q1^HLOC@4x*~68FCLW@g-coY6tjOox6E&gHhg zfIx`X$BoWPOVlq-$kod^_aHq)9w&!OP#4;#eCCt|+OKtmz(>12O7)42uM^nAz_ROi zU9Rg3ZL5&UZbbI<^+9h-O7(j0<=^1?7I?vm6pIi0^u2~2_W=LCG8^jWh>zDZ%iCq0 z4EX6Dx<#L!!(E}Ik6mq)|7aW4z_Owa9G;xMHK3F~x)lL7|+#V?O%pI%2BY*TVS z=!k!T_&kjPWBL-PUdh*g3jIyOYkjoUAitpvV%2kUaw9W}?mpF+Kzv7&`fxW2rotVQ zri1XFg}PAbxV-&YPuRWD7twkVgWAp64m%DUesVw?)u(wdA6=fe&Dgy!r~&cUx%U1& zx%w&BJ(W1HKLGD9nN-ia@KIl)uXq&Ydkux1qpGU1yT5J0K7sh9ia-_oX4B!nY*A@m4#g|v*OL_0OJt<|Ff_0 zvYu;qK>fDpiwP&>?Kbf;t0j^3YDv5Pw3GmN*K3+Rsq%>_E_+~^ z2>uxKKU?)}$(D{sqdqc5KG>n!Vya_#g{Z&ofCkbA1(ddy}QzM@iN7#68UN z+B^>T@~;!So9;!VWmZw!M)|NR#B0Uf5Px<-J+m;c%=EX~17d=HpGo3johm*2AghwX zSt5B9UO8P5dZlZsM7z`p_*C)myf6h)E_OFs-M81wqTk~cb`qpx_pWdL>xd=VZ`?06 z1N!{{CI;;|jrikZ^RxV&CK*&qs{7bGP9opy>(&gf1r@~M0P`3 znZO?&#$c+3ZMqiD9bE>0(CKaaI(~k9@yYgz{`?AaXs-*Y!JVF}eP21qS+AxL2-{_}Oi`fo6{VuP>tg_`a`NVsB% zDyRp93QK4^{9a-`HnU?VU%j1Ja0E|qZBP7WYovqlr0|rb5jTgfC$p;~RMGzAL=BK~ z3nMdzzAptrd^QgAxzA4Z9^=gOf!Zf^r-dYa6Z!?%rn4cuaKZlbPQZloH*Vs{O+&s5 z`iv@yl@|0E6YlU(jX5iWQCuE!4&gKAi~u}}PmR1C9s4XamBk3wu->qrUjzOt&sZhD zDZF+k>hFru(jeBndUY-;=qB`cO&fV{S}#nneJY%UN%4=FFyT^YHx(Ci(+B7rWyZepF+VQT@6hhDj~eEe@|z zLiwxF5YfU5{(hG?>w66Q-C&6CRB%m{{QPmk$3+Vk??UUCU!lp5Sz?oze(IutZxkvv zgO_gGeTRQ40PM|tNT?6q20!@PKjV3tel{oO&)unjA5=*X^NDg6gk%2htG&Z%|r zn9y6OUyWA;`l&MJmAE@WwJhC0D`%V6Tb4m@#M{dc_G>N6#Nr!&IwwIJO#|L*LZ7r*aii1cwA3}Y%W{*E*@ zbJW+=aojJnEtl6dRT&O$N7VFz>XmJD62EIML7Kb3f3_3ws<7A&%aZ;zrrkaDuzwFP z@XmPCOH`%BuJ65+iSk!|YGJ8PfFq^mpifEm^I@omEClM4_A&lYZsH2~t!cSAIXU~Y zy_6ie-2)Im-WaMZ)t`#38Pr~0Y`fHSzck(WJ9lyEI)=GzaU#+ztIW|A<~xSuRxZ0C zzI+%}E0)9`BRyh=i=*djRHOp)hka+n#(R@dd=VbY1Nx6{On_f4v-xC#+OKKg$6aaG zAgy#zM*gdTBu(z{8Izb|w?sv3*8lX2v==iAqYO~Z);9r#$ zYJGVxnCD5bb7@xaSpfbhG3{KJ_<11@QLx{i!eZ63b5^}7?T*B@j@%!1 zymAsN;KLY@+S>JPv@_$#dd_ z2z~zF%2fL52ZH$!88BawJmwMnVe{8}x~|Y4q6hll{Kl}xG95ua^xA?>SNp@xDsVgu ztxvdnnMSS4o6iTfoj!v01JS`hj8L*)(N~WXlk}?c-+JU0llm##2HE4EntV@1Pv^kNjZxzZv2=L|&ol457b}U;t(H>YYm>;YB_t~(%vZSGCAJmTo`goQM zW(T=CRs-xG=qtyx;Hq3&lBJJD{zUkEV}>+lq+cp><|m&j^6^IO`-7ph>ACFbU-?!~ zVSjcJi*i+U=2C7gIhM1N?*3PQmQ}lbud%@z?1Q02M-@gQ-#Sav2=eEnz}KT-UYL&B zS5Bm-!~ITTos{GBt6yo|Y)QR<;=hKP$n5u8?|+xQe>z%_-xRdt$dLo`S4GNG(eKeN zHRk7P#7htPgw-NFcdq_-hED&>!hDN|FfWU!QOBpbYh~Lq)ADORf&RctkM4i@NCgv5vsx%6Tf+2;nkcFXdz<2&WI zgal0JdYL+}*SGsL2mJPtcoBqu()3BT9n2o{d8N9cz2uH`#1C|1?gTW}-*Z_|=`?)lFHz_;Vmb|gNvWFFp=Pp|*_STE4g&2|GD)nh#R zXFQKBoHB7QOWE}u=26&lei{qvx2a{evr{pJo>`yqd;jhb^vCFn3O!5e`enFV*QegY zuJA0v7YV`pA2*+}aGZnv->e)crkpFZbZkAGFY6%Cf8aOvxoE4y{#s~EYscfyTG}T1 z+)A*9_=s3I&@+p-l*eu7hW%5YpYNmQ;#R#9YRnhJ?@>o^eSHV>6VAbWEr_>3k3phz z;6=&hUgax`6Uj}`uK{`w+$(h3pIal=-05j_oa8sc`A}42V9|PnwL(9g<~zeTaYuW* zVLykbX^~0?%%aWj{J_BT9}jcVd|NzmL+jk?lmytH<6MG>P-NMADfYV880$Om{tR3r zmAaPEpjjf1u7{Xv(bI>I(TDtKsaQ7frB`pzb?8rg@~z^2m0&+Rc321CdD&WFFR0`$4;mtydx|_I*-w|_3^~D2aVn3;BWNyQJCSB zinZ^>wn98I$lqPp-LAC5aO}gjZbe1!m>jRDWg546opIYXW6u@9Z|{?}V*|gks83cV zyJmvFjejHD-nI|_jPQp&+}~%;3lSl)mA&F#*MBI9yf?@_F&@J&=U#jFCUMOo!i)JY zBS{+JFkW*bHU;I2;q}srbL8J5JU52~6z>S9p2QbzhJHO7fvxLl4fn$}{ANv{Y}ugj z;Uer;$aima;w4F2Cy6eR@d%&cgcisyV@zsO*6Ew6>iOun*agSWFK`+U&eASHe#H){ z>tJkWN>!*?9d4wmNIU#rKo<(4|)pmn{=|D&twCNKa8Rk zchu@1IdO1)4 z$&_mO+mm?DfzK$ZpGQ7m=*ocgJ8jYJ!6Q^OXTN~@0?aS=(f!`-v2)Mpo$?@u=KRMa zzdKS;KT#WpKmdOt6cegx$~|XlYr<-8!1MP}<6k>f$F1e0i|$`U>ua4@cJ->Fk1It) zI01ZqtiCJ{w_u^qKz4und{`gh<_sG5*wR5QUHAFl(0Q`<|A%KT>oM|0wTc{u4E9H$o z=y9=1xzhgDxkdUU$WJSpAwJVh(%13*memr0?vHhubC(-IwNfkN1Kb~2-%HGrF)Zt) z`N`9NUg=Hc6f+)83bE;!=1;?4!JbWvt8v@gSitWWe8bTBu+DNL(MR%j?7FjcPKpnB z>y|lERPR5qMEygc?@;XG$mPDgcktu3RHW~u?vA@PgPogev?FuCpMJqdL`X9@YW)jcFe~o%YnQ zp=VdJhMphthYj(?WMb>_ub66? z&A#}RYfjq8^8B{@UWy6t-+I~BH!elS{()L~&)(cvadtv7yXA~+)FLT9?qG11MDP36 zS(7n=^lI>ls^{pM1M%VSUxtH!gZ|Fq%0DDGuZv0zdXYV?y=2(cFkjv9edh~zl&{yO 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

^|lH07Yy1<;%PKxr^| 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 R@ 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Ð>ydwr2!2ATe#gs%M? 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°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 z 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 + +#ifdef _WIN32 + #include +#endif + +namespace RE { + class Actor; +} + +#ifndef SWE_DLL_NAME + #define SWE_DLL_NAME "DynamicWetness.dll" +#endif + +namespace SWE { + namespace API { + + // =========================== + // Categories (low 4 bits) + // =========================== + /** + * @brief Material categories targetable by external wetness. + * + * Combine any of the low 4 bits to select affected materials. + * Typical usage: + * unsigned mask = CAT_SKIN_FACE | CAT_HAIR; // skin + hair + */ + static constexpr std::uint32_t CAT_SKIN_FACE = 1u << 0; /// Skin & face materials + static constexpr std::uint32_t CAT_HAIR = 1u << 1; /// Hair + static constexpr std::uint32_t CAT_ARMOR_CLOTH = 1u << 2; /// Armor & clothing + static constexpr std::uint32_t CAT_WEAPON = 1u << 3; /// Weapons + static constexpr std::uint32_t CAT_MASK_4BIT = 0x0Fu; /// Mask of all category bits + + // =========================== + // Behavior flags (high bits) + // =========================== + /** + * @brief Flags that modify how SWE blends your external wetness with its internal system. + * + * These live in the upper bits of the same integer you pass as "catMask". + * You can OR them together with category bits. + */ + static constexpr std::uint32_t FLAG_PASSTHROUGH = + 1u << 16; /// Add AFTER SWE's own mixing/drying (additive post). + static constexpr std::uint32_t FLAG_NO_AUTODRY = 1u << 17; /// Your value won't be reduced by SWE's auto-dry. + static constexpr std::uint32_t FLAG_ZERO_BASE = 1u << 18; /// Base wetness in the marked categories is zeroed. + + /// @brief Handy preset: only Skin, additive post, no auto-dry, zero base contribution. + static constexpr std::uint32_t MASK_SKIN_PASSTHROUGH = + (CAT_SKIN_FACE | FLAG_PASSTHROUGH | FLAG_NO_AUTODRY | FLAG_ZERO_BASE); + + // =========================== + // Environment bit mask + // =========================== + /** + * @brief Bits returned by GetEnvMask() describing the actor's environment this frame. + */ + static constexpr std::uint32_t ENV_WATER = 1u << 0; /// Actor is in water/submerged. + static constexpr std::uint32_t ENV_WET_WEATHER = 1u << 1; /// Precipitation affecting actor (rain/snow). + static constexpr std::uint32_t ENV_NEAR_HEAT = 1u << 2; /// Near a heat source (campfire/forge/etc.). + static constexpr std::uint32_t ENV_UNDER_ROOF = 1u << 3; /// Under roof/cover (heuristic). + static constexpr std::uint32_t ENV_EXTERIOR_OPEN = 1u << 4; /// In exterior and not under cover. + + /** + * @brief Convenience struct for decoded environment state. + */ + struct EnvState { + bool inWater{false}; + bool wetWeather{false}; + bool nearHeat{false}; + bool underRoof{false}; + bool exteriorOpen{false}; + }; + + /** + * @brief Decode ENV_* mask returned by GetEnvMask() into booleans. + * @param m Bitmask from GetEnvMask(actor) + */ + inline EnvState DecodeEnv(std::uint32_t m) { + EnvState e; + e.inWater = (m & ENV_WATER) != 0; + e.wetWeather = (m & ENV_WET_WEATHER) != 0; + e.nearHeat = (m & ENV_NEAR_HEAT) != 0; + e.underRoof = (m & ENV_UNDER_ROOF) != 0; + e.exteriorOpen = (m & ENV_EXTERIOR_OPEN) != 0; + return e; + } + + // =========================== + // C-ABI function signatures + // =========================== + // These match the exported DLL functions exactly. Prefer the safe inline wrappers below. + + using PFN_GetFinalWetness = float(__cdecl*)(RE::Actor*); /// Final mixed wetness [0..1]. + using PFN_GetExternalWetness = float(__cdecl*)(RE::Actor*, const char*); /// Last value set for @key [0..1]. + using PFN_GetBaseWetness = float(__cdecl*)(RE::Actor*); /// Internal/base wetness [0..1]. + using PFN_SetExternalWetness = void(__cdecl*)(RE::Actor*, const char*, float, float); + using PFN_ClearExternalWetness = void(__cdecl*)(RE::Actor*, const char*); + using PFN_SetExternalWetnessMask = void(__cdecl*)(RE::Actor*, const char*, float, float, unsigned int); + using PFN_SetExternalWetnessEx = void(__cdecl*)(RE::Actor*, const char*, float, float, unsigned int, float, + float, float, float, float, float, float); + using PFN_GetActorSubmergeLevel = float(__cdecl*)(RE::Actor*); /// Submerge level [0..1]. + using PFN_IsActorInWater = bool(__cdecl*)(RE::Actor*); + using PFN_IsWetWeatherAround = bool(__cdecl*)(RE::Actor*); + using PFN_IsNearHeatSource = bool(__cdecl*)(RE::Actor*, float); /// radius: Skyrim world units. + using PFN_IsUnderRoof = bool(__cdecl*)(RE::Actor*); + using PFN_IsActorInExteriorWet = bool(__cdecl*)(RE::Actor*); + using PFN_GetEnvMask = unsigned(__cdecl*)(RE::Actor*); + + // Resolved at runtime by Init()/LoadFromModule() + inline PFN_GetFinalWetness pGetFinalWetness = nullptr; + inline PFN_GetExternalWetness pGetExternalWetness = nullptr; + inline PFN_GetBaseWetness pGetBaseWetness = nullptr; + inline PFN_SetExternalWetness pSetExternalWetness = nullptr; + inline PFN_ClearExternalWetness pClearExternalWetness = nullptr; + inline PFN_SetExternalWetnessMask pSetExternalWetnessMask = nullptr; + inline PFN_SetExternalWetnessEx pSetExternalWetnessEx = nullptr; + inline PFN_GetActorSubmergeLevel pGetActorSubmergeLevel = nullptr; + inline PFN_IsActorInWater pIsActorInWater = nullptr; + inline PFN_IsWetWeatherAround pIsWetWeatherAround = nullptr; + inline PFN_IsNearHeatSource pIsNearHeatSource = nullptr; + inline PFN_IsUnderRoof pIsUnderRoof = nullptr; + inline PFN_IsActorInExteriorWet pIsActorInExteriorWet = nullptr; + inline PFN_GetEnvMask pGetEnvMask = nullptr; + + // =========================== + // Loader helpers + // =========================== + /** + * @brief Resolve all SWE_* exports from a given module handle. + * @return true if core functions were found (enough to use the API). + */ + inline bool LoadFromModule(HMODULE h) { +#ifdef _WIN32 + if (!h) return false; + auto gp = [&](const char* n) { return GetProcAddress(h, n); }; + + pGetFinalWetness = (PFN_GetFinalWetness)gp("SWE_GetFinalWetness"); + pGetExternalWetness = (PFN_GetExternalWetness)gp("SWE_GetExternalWetness"); + pGetBaseWetness = (PFN_GetBaseWetness)gp("SWE_GetBaseWetness"); + pSetExternalWetness = (PFN_SetExternalWetness)gp("SWE_SetExternalWetness"); + pClearExternalWetness = (PFN_ClearExternalWetness)gp("SWE_ClearExternalWetness"); + pSetExternalWetnessMask = (PFN_SetExternalWetnessMask)gp("SWE_SetExternalWetnessMask"); + pSetExternalWetnessEx = (PFN_SetExternalWetnessEx)gp("SWE_SetExternalWetnessEx"); + pGetActorSubmergeLevel = (PFN_GetActorSubmergeLevel)gp("SWE_GetActorSubmergeLevel"); + pIsActorInWater = (PFN_IsActorInWater)gp("SWE_IsActorInWater"); + pIsWetWeatherAround = (PFN_IsWetWeatherAround)gp("SWE_IsWetWeatherAround"); + pIsNearHeatSource = (PFN_IsNearHeatSource)gp("SWE_IsNearHeatSource"); + pIsUnderRoof = (PFN_IsUnderRoof)gp("SWE_IsUnderRoof"); + pIsActorInExteriorWet = (PFN_IsActorInExteriorWet)gp("SWE_IsActorInExteriorWet"); + pGetEnvMask = (PFN_GetEnvMask)gp("SWE_GetEnvMask"); + + return pGetFinalWetness && pSetExternalWetness && pSetExternalWetnessMask && pGetEnvMask; +#else + (void)h; + return false; +#endif + } + + /** + * @brief Try to find the module by name (SWE_DLL_NAME), then fallbacks. + */ + inline HMODULE FindModule() { +#ifdef _WIN32 + HMODULE h = GetModuleHandleA(SWE_DLL_NAME); + if (!h) { + // Optional fallback if the DLL is named differently + h = GetModuleHandleA("dynamicwetness.dll"); + } + return h; +#else + return nullptr; +#endif + } + + /** + * @brief One-shot init. Finds the DLL and resolves symbols. + * @param hOverride Pass an explicit module handle if you already have one. + * @return true if initialization succeeded. + */ + inline bool Init(HMODULE hOverride = nullptr) { + HMODULE h = hOverride ? hOverride : FindModule(); + return LoadFromModule(h); + } + + /** + * @brief Check if the core API is available (after Init()). + */ + inline bool IsAvailable() { return pGetFinalWetness != nullptr; } + + // =========================== + // Safe convenience wrappers + // =========================== + + /** + * @brief Final wetness after SWE's internal logic + all external sources. + * @param a Actor pointer + * @return Wetness in [0..1]. Returns 0 if SWE is not available. + */ + inline float GetFinalWetness(RE::Actor* a) { return pGetFinalWetness ? pGetFinalWetness(a) : 0.0f; } + + /** + * @brief Value you last set for @p key on @p a (not the final mixed wetness). + * @param a Actor + * @param key External source identifier (normalized: trimmed + lowercase) + * @return [0..1], 0 if not set or SWE not available. + */ + inline float GetExternalWetness(RE::Actor* a, const char* key) { + return pGetExternalWetness ? pGetExternalWetness(a, key) : 0.0f; + } + + /** + * @brief Internal/base wetness tracked by SWE (before external sources). + * @param a Actor + * @return [0..1], 0 if unavailable. + */ + inline float GetBaseWetness(RE::Actor* a) { return pGetBaseWetness ? pGetBaseWetness(a) : 0.0f; } + + /** + * @brief Set/refresh an external wetness value for @p key on @p a. + * + * If this is the first time @p key is used for @p a and no category was set yet, + * SWE defaults to CAT_SKIN_FACE. Subsequent calls keep the previously configured + * category/flags for this @p key. + * + * @param a Actor + * @param key Your unique source key, e.g. "MyMod:spell". Normalized internally. + * @param v Intensity in [0..1] + * @param durationSec Lifetime in seconds; <= 0 means indefinite (until ClearExternalWetness()). + */ + inline void SetExternalWetness(RE::Actor* a, const char* key, float v, float durationSec) { + if (pSetExternalWetness) pSetExternalWetness(a, key, v, durationSec); + } + + /** + * @brief Remove your external source identified by @p key from @p a. + */ + inline void ClearExternalWetness(RE::Actor* a, const char* key) { + if (pClearExternalWetness) pClearExternalWetness(a, key); + } + + /** + * @brief Set/replace @b category mask and behavior flags for @p key on @p a. + * + * This both sets the value and (re)defines which material categories are affected + * and how SWE blends them (via flags). Use this when you need to change the + * category/flag configuration of an existing key. + * + * @param a Actor + * @param key Your unique source key (normalized internal storage) + * @param v Intensity in [0..1] + * @param durationSec Lifetime; <= 0 = indefinite + * @param catMask Low 4 bits: categories (CAT_*). High bits: flags (FLAG_*). + */ + inline void SetExternalWetnessMask(RE::Actor* a, const char* key, float v, float durationSec, + unsigned catMask) { + if (pSetExternalWetnessMask) pSetExternalWetnessMask(a, key, v, durationSec, catMask); + } + + /** + * @brief Advanced: update shader/material overrides for @p key without altering flags. + * + * Use this to tweak how shiny/specular the result can get per category while keeping + * your previously set flags (e.g., NO_AUTODRY) intact. Parameters use negative + * values to mean "leave unchanged / don't force". + * + * @param a Actor + * @param key Your unique source key (normalized internal storage) + * @param v Intensity in [0..1] + * @param durationSec Lifetime; <= 0 = indefinite + * @param catMask Low 4 bits: categories (CAT_*). (Flags are @b not modified by this call.) + * @param maxGloss [-1 or >=0] Cap for gloss when wet (per-category merge) + * @param maxSpec [-1 or >=0] Cap for specular intensity when wet + * @param minGloss [-1 or >=0] Floor gloss even at low wetness + * @param minSpec [-1 or >=0] Floor specular even at low wetness + * @param glossBoost[-1 or >=0] Additive gloss boost + * @param specBoost [-1 or >=0] Additive specular boost + * @param skinHairMul[-1 or >=0] Extra multiplier applied to skin/hair categories + * + * @note Call SetExternalWetnessMask() first if you need to (re)configure flags. + */ + inline void SetExternalWetnessEx(RE::Actor* a, const char* key, float v, float durationSec, unsigned catMask, + float maxGloss, float maxSpec, float minGloss, float minSpec, float glossBoost, + float specBoost, float skinHairMul) { + if (pSetExternalWetnessEx) + pSetExternalWetnessEx(a, key, v, durationSec, catMask, maxGloss, maxSpec, minGloss, minSpec, glossBoost, + specBoost, skinHairMul); + } + + /** + * @brief Submerge level (0 = dry, 1 = fully submerged). + */ + inline float GetActorSubmergeLevel(RE::Actor* a) { + return pGetActorSubmergeLevel ? pGetActorSubmergeLevel(a) : 0.0f; + } + + /// @brief True if the actor is in water. + inline bool IsActorInWater(RE::Actor* a) { return pIsActorInWater ? pIsActorInWater(a) : false; } + /// @brief True if precipitation affecting the actor is detected (rain/snow). + inline bool IsWetWeatherAround(RE::Actor* a) { return pIsWetWeatherAround ? pIsWetWeatherAround(a) : false; } + /// @brief True if a heat source is found within @p r (Skyrim world units). + inline bool IsNearHeatSource(RE::Actor* a, float r) { + return pIsNearHeatSource ? pIsNearHeatSource(a, r) : false; + } + /// @brief True if the actor is detected to be under a roof/cover. + inline bool IsUnderRoof(RE::Actor* a) { return pIsUnderRoof ? pIsUnderRoof(a) : false; } + /// @brief True if actor is in "exterior wet" area (outside & exposed). + inline bool IsActorInExteriorWet(RE::Actor* a) { + return pIsActorInExteriorWet ? pIsActorInExteriorWet(a) : false; + } + + /** + * @brief Raw environment mask (see ENV_*). Prefer DecodeEnv() for convenience. + */ + inline unsigned GetEnvMask(RE::Actor* a) { return pGetEnvMask ? pGetEnvMask(a) : 0u; } + + /** + * @brief Helper to build a category mask (no flags). + */ + inline unsigned MakeCatMask(bool skin, bool hair, bool armor, bool weapon) { + unsigned m = 0; + if (skin) m |= CAT_SKIN_FACE; + if (hair) m |= CAT_HAIR; + if (armor) m |= CAT_ARMOR_CLOTH; + if (weapon) m |= CAT_WEAPON; + return m; + } + + } +} diff --git a/package/Shaders/Common/LightingCommon.hlsli b/package/Shaders/Common/LightingCommon.hlsli index f229212529..aa50b02c95 100644 --- a/package/Shaders/Common/LightingCommon.hlsli +++ b/package/Shaders/Common/LightingCommon.hlsli @@ -71,6 +71,17 @@ struct MaterialProperties # endif float Roughness; float3 F0; +# if defined(CS_SKIN) && defined(SKIN) + float RoughnessSecondary; + float SecondarySpecIntensity; + float Curvature; + float Thickness; + float3 SubsurfaceColor; + float AO; + float FuzzRoughness; + float3 FuzzColor; + float FuzzWeight; +# endif #else float Roughness; float Metallic; diff --git a/package/Shaders/Common/LightingEval.hlsli b/package/Shaders/Common/LightingEval.hlsli index 2e8699e583..21748a03ec 100644 --- a/package/Shaders/Common/LightingEval.hlsli +++ b/package/Shaders/Common/LightingEval.hlsli @@ -106,6 +106,29 @@ void EvaluateLighting(DirectContext context, MaterialProperties material, float3 Hair::GetHairDirectLight(lightingOutput, context, material, tbnTr, uv); return; } +# endif +# if defined(SKIN) && defined(CS_SKIN) + if (SharedData::skinData.skinParams.w > 0.0f) { + Skin::SkinDirectLightInput(lightingOutput, context, material); + float3 softLightColor = context.lightColor * context.softShadow; + + // SSS fallback for forward skin rendering +# if !defined(DEFERRED) + const float NdotL = dot(context.worldNormal, context.lightDir); +# if defined(SOFT_LIGHTING) + lightingOutput.diffuse += softLightColor * GetSoftLightMultiplier(NdotL) * material.rimSoftLightColor; +# endif + +# if defined(RIM_LIGHTING) + lightingOutput.diffuse += softLightColor * GetRimLightMultiplier(context.lightDir, context.viewDir, context.worldNormal) * material.rimSoftLightColor; +# endif + +# if defined(BACK_LIGHTING) + lightingOutput.diffuse += softLightColor * saturate(-NdotL) * material.backLightColor; +# endif +# endif + return; + } # endif const float NdotL = dot(context.worldNormal, context.lightDir); float3 diffuseLightColor = context.lightColor * context.detailedShadow; @@ -137,6 +160,12 @@ void GetIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext Hair::GetHairIndirectLobeWeights(lobeWeights, context, material, uv); return; } +# endif +# if defined(SKIN) && defined(CS_SKIN) + if (SharedData::skinData.skinParams.w > 0.0f) { + Skin::SkinIndirectLobeWeights(lobeWeights, material, context); + return; + } # endif lobeWeights.diffuse = material.BaseColor; # if defined(DYNAMIC_CUBEMAPS) diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 8168507571..05c257b034 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -275,6 +275,17 @@ namespace SharedData uint3 pad; }; + struct SkinData + { + float4 skinParams; + float4 skinParams2; + float4 skinDetailParams; + float4 sssParams; + float4 fuzzParams; + float4 physicalParams; + float4 wetParams; + }; + cbuffer FeatureData : register(b6) { GrassLightingSettings grassLightingSettings; @@ -294,6 +305,7 @@ namespace SharedData TerrainBlendingSettings terrainBlendingSettings; ExponentialHeightFogSettings exponentialHeightFogSettings; TruePBRSettings truePBRSettings; + SkinData skinData; }; Texture2D DepthTexture : register(t17); diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 60d66b7ecf..3a7455c8dd 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -552,6 +552,12 @@ Texture2D TexLandLodNoiseSampler : register(t15); Texture2D TexShadowMaskSampler : register(t14); +# if defined(SKIN) && defined(CS_SKIN) +Texture2D TexSkinExtraSampler : register(t71); +Texture2D TexSkinWetnessSampler : register(t74); +Texture2D TexSkinWetnessNormalSampler : register(t75); +# endif + cbuffer PerTechnique : register(b0) { float4 FogColor : packoffset(c0); // Color in xyz, invFrameBufferRange in w @@ -937,6 +943,10 @@ float GetSnowParameterY(float texProjTmp, float alpha) # define ANISOTROPIC_ALPHA # endif +# if defined(CS_SKIN) +# include "Skin/Skin.hlsli" +# endif + # define LinearSampler SampColorSampler # include "Common/ShadowSampling.hlsli" @@ -1329,6 +1339,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float4 baseColor = 0; float4 normal = 0; float glossiness = 0; +# if defined(CS_SKIN) + const bool skinEnabled = SharedData::skinData.skinParams.w > 0.0f; +# if defined(SKIN) + float skinRoughness = 0; + float skinSpecular = 0; + float skinFuzzMask = 1; + float skinWetMask = 1; + float skinAO = 1; + bool skinRoughnessSet = false; +# endif +# endif float4 rawRMAOS = 0; @@ -1866,6 +1887,47 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # endif // LOD_BLENDING +# if defined(SKIN) && defined(CS_SKIN) + float4 skinsk = 0; + float4 skinExtra = 0; + float4 skinWetnessSample = 0; + uint2 skinExtraDimensions = uint2(0, 0); + uint2 wetnessDimensions = uint2(0, 0); + bool hasSkinExtra = false; + bool hasSkinWetness = false; + if (skinEnabled) { + skinsk = TexRimSoftLightWorldMapOverlaySampler.Sample(SampRimSoftLightWorldMapOverlaySampler, uv); + TexSkinExtraSampler.GetDimensions(skinExtraDimensions.x, skinExtraDimensions.y); + TexSkinWetnessSampler.GetDimensions(wetnessDimensions.x, wetnessDimensions.y); + hasSkinExtra = skinExtraDimensions.x > 32 && skinExtraDimensions.y > 32; + hasSkinWetness = wetnessDimensions.x > 32 && wetnessDimensions.y > 32; + } + float4 skinWetnessNormal = float4(0.f, 0.f, 0.f, 1.f); + + if (hasSkinExtra && SharedData::skinData.skinParams.x > 0.0f) { + skinExtra = TexSkinExtraSampler.Sample(SampColorSampler, uv); + skinRoughness = skinExtra.x; + skinFuzzMask = skinExtra.y; + skinAO = skinExtra.z; + skinSpecular = skinExtra.w; + skinRoughnessSet = true; + } else { + skinRoughnessSet = false; + } + if (hasSkinWetness && skinEnabled) { + skinWetnessSample = TexSkinWetnessSampler.Sample(SampColorSampler, uv); + if ((skinWetnessSample.y == 0 && skinWetnessSample.z == 0) || (skinWetnessSample.x == skinWetnessSample.y && skinWetnessSample.y == skinWetnessSample.z && skinWetnessSample.w >= 0.99f)) { + skinWetMask = skinWetnessSample.x; + skinWetnessNormal.xyz = Skin::CalculateNormalFromHeight(skinWetMask, SharedData::skinData.wetParams.w * 0.0001, uv) * 0.5 + 0.5; + } else { + skinWetnessNormal.xyz = skinWetnessSample.xyz; + skinWetMask = skinWetnessSample.w; + } + } else { + skinWetMask = 1.0; + } +# endif + float landSnowMask1 = GetLandSnowMaskValue(baseColor.w); # if defined(MODELSPACENORMALS) @@ -1928,6 +1990,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif // FACEGEN +# if defined(SKIN) && defined(CS_SKIN) + if (skinEnabled) { + baseColor.xyz = baseColor.xyz * SharedData::skinData.skinParams2.w; + } +# endif // CS_SKIN + # if defined(HAIR) && defined(CS_HAIR) float3 hairTint = 0; @@ -2023,6 +2091,59 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPARKLE # endif // defined (MODELSPACENORMALS) && !defined (SKINNED) +# if defined(SKIN) && defined(CS_SKIN) +# if defined(WETNESS_EFFECTS) + float3 skinWetNormal = worldNormal.xyz; +# if defined(FACEGEN) + float2 wetUV = uv; +# else + float2 wetUV = uv * SharedData::skinData.skinDetailParams.y; +# endif + float2 dynamicWet = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); + float skinWetness = Skin::PerlinNoise(wetUV, SharedData::skinData.wetParams.x, SharedData::skinData.wetParams.y, SharedData::skinData.wetParams.z, clamp(dynamicWet.x + dynamicWet.y + SharedData::skinData.skinParams2.y, 0.f, 2.f) * (hasSkinWetness ? 1.0 : 0.5)); + if ((SharedData::skinData.skinDetailParams.w > 0.0f || skinWetness > 0.0f) && skinEnabled) +# else + if (SharedData::skinData.skinDetailParams.w > 0.0f && skinEnabled) +# endif + { +# if defined(FACEGEN) + float2 detailUV = input.TexCoord0.xy * SharedData::skinData.skinDetailParams.x; +# else + float2 detailUV = input.TexCoord0.xy * SharedData::skinData.skinDetailParams.x * SharedData::skinData.skinDetailParams.y; +# endif // FACEGEN +# if defined(MODELSPACENORMALS) + const float3x3 tbnTr = Skin::ReconstructTBN(input.WorldPosition.xyz, worldNormal, screenUV); + const float3x3 tbn = transpose(tbnTr); + const float3 tangentNormal = mul(tbnTr, worldNormal.xyz); +# else + const float3 tangentNormal = normal.xyz; +# endif // MODELSPACENORMALS + float3 detailNormal = float3(Skin::TexSkinDetailNormal.SampleBias(SampNormalSampler, detailUV, SharedData::MipBias - 1.0f).xy, 0.5f); + skinAO *= Skin::TexSkinDetailNormal.Sample(SampNormalSampler, detailUV).w; + detailNormal = (detailNormal * 2.0 - 1.0) * SharedData::skinData.skinDetailParams.z; + float3 combinedTangentNormal = normalize(float3(Skin::ReorientNormal(detailNormal, tangentNormal).xy, tangentNormal.z)); + float3 combinedNormal = normalize(mul(tbn, combinedTangentNormal)); + if (SharedData::skinData.skinDetailParams.w > 0.0f) + worldNormal.xyz = combinedNormal; +# if defined(WETNESS_EFFECTS) + if (skinWetness > 0.0f) { + float3 wetNormal = Skin::CalculateNormalFromHeight(skinWetness, SharedData::skinData.wetParams.w * 0.0005, uv); + if (hasSkinWetness) { + // float3 wetMaskNormal = Skin::CalculateNormalFromHeight(skinWetMask, SharedData::skinData.wetParams.w * 0.00005, uv); + float3 wetMaskNormal = (skinWetnessNormal.xyz * 2.0 - 1.0); + wetNormal = Skin::ReorientNormal(wetMaskNormal, wetNormal); + } + if (SharedData::skinData.skinParams2.y > 1.0f) { + wetNormal = lerp(wetNormal, tangentNormal, saturate(SharedData::skinData.skinParams2.y - 1.0f)); + } + float3 combinedWetNormal = skinWetMask ? wetNormal : combinedTangentNormal; + skinWetNormal = normalize(mul(tbn, combinedWetNormal)); + skinWetNormal = lerp(worldNormal.xyz, skinWetNormal, skinWetness > 0 ? 1 : 0); + } +# endif + } +# endif // CS_SKIN + float2 baseShadowUV = 1.0.xx; float4 shadowColor = 1.0; if ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) && ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir) || inWorld) || numShadowLights > 0) { @@ -2267,6 +2388,34 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # endif // TRUE_PBR +# if defined(SKIN) && defined(CS_SKIN) + const float ExtraRoughness = BRDF::F_Schlick(0.04, saturate(dot(worldNormal.xyz, viewDirection))) * SharedData::skinData.fuzzParams.w; + material.Roughness = SharedData::skinData.skinParams.x; + material.Roughness = saturate(SharedData::skinData.skinParams.x - SharedData::skinData.skinParams.z * material.Glossiness); + material.RoughnessSecondary = SharedData::skinData.skinParams.y; + if (skinRoughnessSet) { + material.Roughness = skinRoughness * SharedData::skinData.physicalParams.x; + material.RoughnessSecondary = skinRoughness * SharedData::skinData.physicalParams.y; + } + material.Roughness = min(1.0, material.Roughness + ExtraRoughness); + material.RoughnessSecondary = min(1.0, material.RoughnessSecondary + ExtraRoughness); + material.SecondarySpecIntensity = SharedData::skinData.skinParams2.x; + material.Thickness = 1 - skinsk.x; + material.SubsurfaceColor = skinsk.xyz; + material.F0 = SharedData::skinData.skinParams2.zzz; + material.AO = skinAO; + material.Curvature = Skin::CalculateCurvature(worldNormal.xyz); + + material.FuzzWeight = SharedData::skinData.fuzzParams.x; + material.FuzzRoughness = SharedData::skinData.fuzzParams.y; + material.FuzzColor = SharedData::skinData.fuzzParams.zzz; + + if (skinRoughnessSet) { + material.F0 = 0.08f * skinSpecular * SharedData::skinData.physicalParams.z; + material.FuzzWeight *= skinFuzzMask; + } +# endif // CS_SKIN + # if defined(CS_HAIR) && defined(HAIR) if (SharedData::hairSpecularSettings.Enabled) { material.Shininess = SharedData::hairSpecularSettings.HairGlossiness; @@ -2426,6 +2575,19 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) rainWetness = SharedData::wetnessEffectsSettings.SkinWetness * SharedData::wetnessEffectsSettings.Wetness; # endif +# if defined(CS_SKIN) && !defined(SKIN) + if (skinEnabled) { + float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); +# if defined(TRUE_PBR) + dynamicWetness.x = lerp(dynamicWetness.x, 0.0f, material.Metallic); +# endif + float dynamicWetnessValue = clamp(dynamicWetness.x + dynamicWetness.y, 0.f, 2.f); +# if defined(HAIR) + dynamicWetnessValue = min(SharedData::skinData.skinParams2.y + dynamicWetnessValue, 2.0f); +# endif + rainWetness += min(dynamicWetnessValue, 1.f); + } +# endif float shoreWetness = shoreFactor * SharedData::wetnessEffectsSettings.MaxShoreWetness; wetness = max(shoreWetness, rainWetness); @@ -2433,7 +2595,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float puddleWetness = SharedData::wetnessEffectsSettings.PuddleWetness * minWetnessAngle; float puddle = wetness; -# if !defined(SKINNED) +# if !defined(SKINNED) && !(defined(SKIN) && defined(CS_SKIN)) if (wetness > 0.0 || puddleWetness > 0.0) { float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; puddle = Random::perlinNoise(puddleCoords) * 0.5 + 0.5; @@ -2462,6 +2624,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 rippleNormal = normalize(lerp(float3(0, 0, 1), raindropInfo.xyz, lerp(flatnessAmount, 1.0, 0.5))); wetnessNormal = WetnessEffects::ReorientNormal(rippleNormal, wetnessNormal); +# if defined(SKIN) && defined(CS_SKIN) + if (skinEnabled && (skinWetness > 0.0f)) { + wetnessNormal = skinWetNormal; + wetnessGlossinessSpecular = saturate(max(wetnessGlossinessSpecular, skinWetness)); + } +# endif + // Minimum roughness prevents an extreme retroreflective peak (NdotH→1) for near-zero // roughness puddles. Real water has ripples and surface tension that keep it from being // optically perfect; the ripple normal map adds micro-variation but GGX still peaks diff --git a/src/Feature.cpp b/src/Feature.cpp index aa0e23992f..b5e7287a79 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -22,6 +22,7 @@ #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/ScreenshotFeature.h" +#include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" @@ -245,7 +246,8 @@ const std::vector& Feature::GetFeatureList() &globals::features::linearLighting, &globals::features::unifiedWater, &globals::features::exponentialHeightFog, - &globals::features::hdrDisplay + &globals::features::hdrDisplay, + &globals::features::skin }; if (REL::Module::IsVR()) { diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 98f5aa834b..f68f563e57 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -11,6 +11,7 @@ #include "Features/LODBlending.h" #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" +#include "Features/Skin.h" #include "Features/Skylighting.h" #include "Features/TerrainBlending.h" #include "Features/TerrainShadows.h" @@ -53,5 +54,6 @@ std::pair GetFeatureBufferData(bool a_inWorld) globals::features::linearLighting.GetCommonBufferData(), globals::features::terrainBlending.settings, globals::features::exponentialHeightFog.settings, - globals::features::truePBR.settings); + globals::features::truePBR.settings, + globals::features::skin.GetCommonBufferData()); } \ No newline at end of file diff --git a/src/Features/Skin.cpp b/src/Features/Skin.cpp new file mode 100644 index 0000000000..9bd9bde1a5 --- /dev/null +++ b/src/Features/Skin.cpp @@ -0,0 +1,625 @@ +#include "Skin.h" +#include + +#include "Deferred.h" +#include "Globals.h" +#include "Hooks.h" +#include "Menu.h" +#include "ShaderCache.h" +#include "State.h" + +#include "DynamicWetness_PublicAPI.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Skin::Settings, + EnableSkin, + SkinMainRoughness, + SkinSecondRoughness, + SkinSpecularTexMultiplier, + SecondarySpecularStrength, + F0, + BaseColorMultiplier, + PhysicalMainRoughnessMultiplier, + PhysicalSecondRoughnessMultiplier, + PhysicalSpecularStrength, + ExtraEdgeRoughness, + EnableSkinDetail, + SkinDetailStrength, + SkinDetailTiling, + BodyTilingMultiplier, + ExtraSkinWetness, + WetFadeTime, + StartSweat, + FullSweat, + WetParams, + Translucency, + sssWidth, + UseSSS, + FuzzStrength, + FuzzRoughness, + FuzzF0, + UseDynamicWetness); + +void Skin::DrawSettings() +{ + ImGui::Checkbox("Enable Advanced Skin", &settings.EnableSkin); + + ImGui::Text("Advanced Skin Shader using dual specular lobes."); + + ImGui::Spacing(); + ImGui::SliderFloat("Primary Roughness", &settings.SkinMainRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls microscopic roughness of stratum corneum layer"); + } + + ImGui::SliderFloat("Secondary Roughness", &settings.SkinSecondRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Smoothness of epidermal cell layer reflections"); + ImGui::BulletText("Should be 30-50%% lower than Primary"); + } + + ImGui::SliderFloat("Specular Texture Multiplier", &settings.SkinSpecularTexMultiplier, 0.0f, 10.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiplier for specular map"); + ImGui::BulletText("A multiplier for the vanilla specular map, applied to the first layer's roughness"); + } + + ImGui::SliderFloat("Secondary Specular Strength", &settings.SecondarySpecularStrength, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Intensity of secondary specular highlights"); + } + + ImGui::SliderFloat("Fresnel F0", &settings.F0, 0.0f, 0.1f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Fresnel reflectance"); + } + + ImGui::SliderFloat("Base Color Multiplier", &settings.BaseColorMultiplier, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiplier for the base color texture"); + } + + ImGui::Spacing(); + ImGui::Text("Options for additional roughness and specular maps."); + + ImGui::SliderFloat("Physical Main Roughness Multiplier", &settings.PhysicalMainRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Physical Second Roughness Multiplier", &settings.PhysicalSecondRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Physical Specular Multiplier", &settings.PhysicalSpecularStrength, 0.0f, 2.0f, "%.2f"); + + ImGui::Spacing(); + + ImGui::SliderFloat("Extra Edge Roughness", &settings.ExtraEdgeRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Extra roughness at the edges of the skin, to approximate peach fuzz on the face."); + } + + ImGui::SliderFloat("Fuzz Strength", &settings.FuzzStrength, 0.0f, 2.0f, "%.2f"); + + ImGui::SliderFloat("Fuzz Roughness", &settings.FuzzRoughness, 0.1f, 1.0f, "%.2f"); + + ImGui::SliderFloat("Fuzz F0", &settings.FuzzF0, 0.0f, 0.5f, "%.4f"); + + ImGui::Spacing(); + + ImGui::Checkbox("Enable SSS Transmission", &settings.UseSSS); + + ImGui::SliderFloat("Translucency", &settings.Translucency, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Translucency of the SSS Transmittance effect"); + } + + ImGui::SliderFloat("SSS Width", &settings.sssWidth, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Width of the SSS Transmittance effect"); + } + + ImGui::Spacing(); + + ImGui::SliderFloat("Extra Skin Wetness", &settings.ExtraSkinWetness, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort."); + } + + ImGui::SliderFloat("Wetness Fade Out Time", &settings.WetFadeTime, 0.0f, 50.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How many seconds it takes for skin to fully dry after leaving water. Higher values mean wetness lingers longer."); + } + + if (isDynamicWetnessAvailable) { + ImGui::Text("Dynamic Wetness detected."); + ImGui::Checkbox("Use Dynamic Wetness", &settings.UseDynamicWetness); + } else { + settings.UseDynamicWetness = false; + } + + if (!settings.UseDynamicWetness) { + ImGui::SliderFloat("Stamina Threshold for Sweat", &settings.StartSweat, 0.0f, 1.0f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The character starts sweating when their stamina drops below this percentage. For example, 0.75 means sweat appears below 75%% stamina."); + } + ImGui::SliderFloat("Full Sweat Threshold", &settings.FullSweat, 0.0f, 1.0f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The character reaches maximum sweat when stamina drops below this percentage. For example, 0.15 means full sweat below 15%% stamina."); + } + } + + ImGui::SliderFloat("Wetness Perlin Noise Scale", &settings.WetParams.x, 0.0f, 1024.0f, "%1.f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls the size of the wet/dry pattern on skin. Higher values create a finer, more detailed pattern; lower values produce larger, broader wet patches."); + } + ImGui::SliderFloat("Wetness Perlin Noise Lacunarity", &settings.WetParams.y, 0.0f, 2.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls how much fine detail is added to the wetness pattern. Higher values add more small-scale variation on top of the base pattern."); + } + ImGui::SliderFloat("Wetness Perlin Noise Persistence", &settings.WetParams.z, 0.0f, 20.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls the overall contrast and roughness of the wetness pattern. Higher values make the pattern more pronounced and varied."); + } + ImGui::SliderFloat("Wetness Normal Scale", &settings.WetParams.w, 0.0f, 20.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls how bumpy wet skin appears. Higher values create more visible surface ripples and distortion on wet areas."); + } + + ImGui::Spacing(); + + ImGui::Checkbox("Enable Skin Detail", &settings.EnableSkinDetail); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable skin detail texture"); + } + + ImGui::SliderFloat("Skin Detail Strength", &settings.SkinDetailStrength, -2.0f, 2.0f); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Strength of skin detail texture"); + } + + ImGui::SliderFloat("Skin Detail Tiling", &settings.SkinDetailTiling, 1.0f, 50.0f, "%1.f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The more tiling, the more detailed the skin will be"); + } + + ImGui::SliderFloat("Body Tiling Multiplier", &settings.BodyTilingMultiplier, 0.5f, 5.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiply the tiling for the body to match the face"); + } + + if (ImGui::Button("Reload Skin Detail Texture")) { + ReloadSkinDetail(); + } + + BUFFER_VIEWER_NODE(texSkinDetail, 1.0f) +} + +void Skin::LoadSkinDetailTexture() +{ + auto device = globals::d3d::device; + + DirectX::ScratchImage image; + try { + std::filesystem::path path{ "Data\\Shaders\\Skin\\skin_detail_n.dds" }; + DX::ThrowIfFailed(LoadFromDDSFile(path.c_str(), DirectX::DDS_FLAGS_NONE, nullptr, image)); + } catch (const DX::com_exception& e) { + logger::error("{}", e.what()); + return; + } + + ID3D11Resource* pResource = nullptr; + try { + DX::ThrowIfFailed(CreateTexture(device, + image.GetImages(), image.GetImageCount(), + image.GetMetadata(), &pResource)); + } catch (const DX::com_exception& e) { + logger::error("{}", e.what()); + return; + } + + texSkinDetail = eastl::make_unique(reinterpret_cast(pResource)); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texSkinDetail->desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = static_cast(image.GetMetadata().mipLevels) } + }; + texSkinDetail->CreateSRV(srvDesc); +} + +void Skin::SetupResources() +{ + logger::debug("Loading skin detail texture..."); + LoadSkinDetailTexture(); + + PerGeometryCB = eastl::make_unique(ConstantBufferDesc()); + + // Check for Dynamic Wetness availability + isDynamicWetnessAvailable = SWE::API::Init(); +} + +void Skin::ReloadSkinDetail() +{ + logger::debug("Reloading skin detail texture..."); + LoadSkinDetailTexture(); +} + +void Skin::Prepass() +{ + auto context = globals::d3d::context; + + if (texSkinDetail) { + ID3D11ShaderResourceView* srv = texSkinDetail->srv.get(); + context->PSSetShaderResources(72, 1, &srv); + } +} + +struct SKIN_BSLightingShader_SetupMaterial +{ + static void thunk(RE::BSLightingShader* shader, RE::BSLightingShaderMaterialBase const* material) + { + func(shader, material); + + auto& skin = globals::features::skin; + if (skin.loaded) { + skin.BSLightingShader_SetupMaterial(material); + } + } + static inline REL::Relocation func; +}; + +void Skin::PostPostLoad() +{ + logger::info("[Advanced Skin] Hooking BSLightingShader::SetupMaterial"); + stl::write_vfunc<0x4, SKIN_BSLightingShader_SetupMaterial>(RE::VTABLE_BSLightingShader[0]); + Hooks::Install(); +} + +Skin::SkinData Skin::GetCommonBufferData() +{ + SkinData data{}; + data.skinParams = float4(settings.SkinMainRoughness, settings.SkinSecondRoughness, settings.SkinSpecularTexMultiplier, float(settings.EnableSkin)); + data.skinParams2 = float4(settings.SecondarySpecularStrength, settings.ExtraSkinWetness, settings.F0, settings.BaseColorMultiplier); + data.skinDetailParams = float4(settings.SkinDetailTiling, settings.BodyTilingMultiplier, settings.SkinDetailStrength, float(settings.EnableSkinDetail && settings.EnableSkin)); + data.sssParams = float4(settings.Translucency, settings.sssWidth, 0.0f, float(settings.UseSSS)); + data.fuzzParams = float4(settings.FuzzStrength, settings.FuzzRoughness, settings.FuzzF0, settings.ExtraEdgeRoughness); + data.physicalParams = float4(settings.PhysicalMainRoughnessMultiplier, settings.PhysicalSecondRoughnessMultiplier, settings.PhysicalSpecularStrength, 0.0f); + data.wetParams = settings.WetParams; + return data; +} + +void Skin::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void Skin::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void Skin::RestoreDefaultSettings() +{ + settings = {}; +} + +// By PO3 +// https://github.com/powerof3/Splashes-of-Skyrim/blob/master/src/Manager.cpp +float Skin::GetWaterHeight(const RE::TESObjectREFR* a_ref, const RE::NiPoint3& a_pos) +{ + float waterHeight = -RE::NI_INFINITY; + + if (const auto waterManager = RE::TESWaterSystem::GetSingleton()) { + waterHeight = a_ref->GetWaterHeight(); + + if (waterHeight != -RE::NI_INFINITY) { + return waterHeight; + } + + const auto get_nearest_water_object_height = [&]() { + for (const auto& waterObject : waterManager->waterObjects) { + if (waterObject) { + for (const auto& bound : waterObject->multiBounds) { + if (bound) { + if (auto size{ bound->size }; size.z <= 10.0f) { //avoid sloped water + auto center{ bound->center }; + const auto boundMin = center - size; + const auto boundMax = center + size; + if (!(a_pos.x < boundMin.x || a_pos.x > boundMax.x || a_pos.y < boundMin.y || a_pos.y > boundMax.y)) { + return center.z; + } + } + } + } + } + } + + return -RE::NI_INFINITY; + }; + + waterHeight = get_nearest_water_object_height(); + } + + return waterHeight; +} + +float4 Skin::GetWetness(RE::BSGeometry* geometry) +{ + float4 wetness = float4(0.0f, 0.0f, 0.0f, 0.0f); + if (auto userData = geometry->GetUserData()) + if (auto actor = userData->As()) { + const float positionZ = actor->GetPositionZ(); + wetness.z = positionZ; + if (settings.UseDynamicWetness && isDynamicWetnessAvailable) { + float dynamicWetness = SWE::API::GetFinalWetness(actor); + wetness.x = dynamicWetness; + } else { + const float stamina = actor->AsActorValueOwner()->GetActorValue(RE::ActorValue::kStamina); + const float permanentStamina = actor->AsActorValueOwner()->GetPermanentActorValue(RE::ActorValue::kStamina); + const float temporaryStamina = actor->GetActorValueModifier(RE::ACTOR_VALUE_MODIFIER::kTemporary, RE::ActorValue::kStamina); + const float maxStamina = std::max(permanentStamina + temporaryStamina, 1.0f); + const float staminaPercentage = actor->IsDead() ? 1.0f : (stamina / maxStamina); + const float sweatRange = settings.StartSweat - settings.FullSweat; + wetness.x = (std::abs(sweatRange) < 1e-5f) ? 0.0f : + (staminaPercentage >= settings.StartSweat) ? 0.0f : + (staminaPercentage <= settings.FullSweat) ? 1.0f : + (settings.StartSweat - staminaPercentage) / sweatRange; + } + if (actor->IsInWater()) { + wetness.y = 2.0f; + float waterHeight = -RE::NI_INFINITY; + const uint32_t formID = actor->AsReference()->formID; + const uint currentFrame = globals::state->frameCount; + auto cacheIt = waterHeightCache.find(formID); + if (cacheIt != waterHeightCache.end() && cacheIt->second.frameCount == currentFrame) { + waterHeight = cacheIt->second.waterHeight; + } else { + waterHeight = GetWaterHeight(actor->AsReference(), actor->GetPosition()); + waterHeightCache[formID] = { currentFrame, waterHeight }; + } + wetness.w = std::max(0.0f, waterHeight - positionZ); + } else { + wetness.y = 0.0f; + wetness.w = 0.0f; + } + + const uint32_t actorFormID = actor->AsReference()->formID; + + // Prevent unbounded growth: clear stale entries periodically + if (actorWetnessMap.size() > 1024) { + actorWetnessMap.clear(); + } + + auto it = actorWetnessMap.find(actorFormID); + if (it != actorWetnessMap.end()) { + auto& cached = it->second; + + const float fadeTime = std::max(settings.WetFadeTime, 0.001f); + if (cached.x < wetness.x) { + cached.x = wetness.x; + } else if (cached.x > wetness.x) { + cached.x -= *globals::game::deltaTime / fadeTime; + cached.x = std::max(cached.x, 0.0f); + wetness.x = cached.x; + } + + if (cached.y < wetness.y) { + cached.y = wetness.y; + if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else if (cached.y > wetness.y) { + cached.y -= *globals::game::deltaTime / fadeTime; + cached.y = std::max(cached.y, 0.0f); + wetness.y = cached.y; + if (wetness.y == 0.0f) { + wetness.w = 0.0f; + cached.w = 0.0f; + } else if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else { + actorWetnessMap.emplace(actorFormID, wetness); + } + } + return wetness; +} + +struct SkinExtendedRendererState +{ + uint32_t PSResourceModifiedBits = 0; + std::array PSTexture; + + void SetExtraSkinPSTexture(RE::BSGraphics::Texture* newTexture, RE::BSGraphics::Texture* newTexture2) + { + { + PSTexture = { + newTexture ? newTexture->resourceView : nullptr, + newTexture2 ? newTexture2->resourceView : nullptr + }; + PSResourceModifiedBits = 1; + } + } + + SkinExtendedRendererState() + { + PSTexture.fill(nullptr); + } +} skinExtendedRendererState; + +void Skin::SetupExtraTexture(RE::BSLightingShaderMaterialBase const* material, RE::BSTextureSet* inTextureSet, uint32_t i_hashKey) +{ + if (!inTextureSet || material->normalTexture == nullptr) { + logger::error("[Advanced Skin] SetupExtraTexture : Texture set is null for material: {}", i_hashKey); + return; + } + + uint32_t hashKey = 0; + hashKey = material->hashKey; + if (hashKey == 0 || hashKey != i_hashKey) { + logger::error("[Advanced Skin] SetupExtraTexture : Invalid hash key for material: {}", i_hashKey); + return; + } + + const char extraTextureName[] = "_rfaos.dds"; + const char wetnessTextureName[] = "_wet.dds"; + const char* workingNormalPath = nullptr; + const char* workingSpecularPath = nullptr; + auto workingMaterial = static_cast(material); + auto hasSpecular = workingMaterial->specularBackLightingTexture != nullptr; + + auto graphicsState = globals::game::graphicsState; + const auto& stateData = graphicsState->GetRuntimeData(); + + if (hasSpecular) { + if (auto specularPath = inTextureSet->GetTexturePath(RE::BSTextureSet::Texture::kSpecular)) { + workingSpecularPath = specularPath; + } + } + if (auto normalPath = inTextureSet->GetTexturePath(RE::BSTextureSet::Texture::kNormal)) { + workingNormalPath = normalPath; + } else { + logger::error("[Advanced Skin] SetupExtraTexture : No specular or normal texture found in texture set from material: {}", hashKey); + auto& workingExtraPtr = skinExtraTextures.try_emplace(hashKey).first->second; + workingExtraPtr.rfaosTexture = stateData.defaultTextureBlack; + workingExtraPtr.wetnessTexture = stateData.defaultTextureBlack; + workingExtraPtr.extraTexturePath = ""; + workingExtraPtr.wetnessTexturePath = ""; + workingExtraPtr.hasExtraTexture = false; + workingExtraPtr.hasWetnessTexture = false; + return; + } + + const char* foundPath = nullptr; + std::string extraTexturePath = ""; + std::string wetnessTexturePath = ""; + + auto findIgnoreCase = [](std::string_view str, std::string_view pattern) -> size_t { + auto it = std::search(str.begin(), str.end(), pattern.begin(), pattern.end(), + [](char ch1, char ch2) { return std::tolower(ch1) == std::tolower(ch2); }); + return it == str.end() ? std::string_view::npos : std::distance(str.begin(), it); + }; + + auto tryReplaceSuffix = [&](const char* basePath, std::string_view suffix) -> bool { + auto pos = findIgnoreCase(basePath, suffix); + if (pos == std::string_view::npos) + return false; + extraTexturePath = std::string(basePath); + wetnessTexturePath = std::string(basePath); + extraTexturePath.replace(pos, suffix.size(), extraTextureName); + wetnessTexturePath.replace(pos, suffix.size(), wetnessTextureName); + foundPath = basePath; + return true; + }; + + if (hasSpecular && workingSpecularPath) { + tryReplaceSuffix(workingSpecularPath, "_s.dds"); + } + + if (!foundPath && workingNormalPath) { + if (!tryReplaceSuffix(workingNormalPath, "_n.dds")) { + if (!tryReplaceSuffix(workingNormalPath, "_msn.dds")) { + tryReplaceSuffix(workingNormalPath, ".dds"); + } + } + } + + logger::debug("[Advanced Skin] SetupExtraTexture : Extra texture path: {} for {}", extraTexturePath, foundPath ? foundPath : "(none)"); + logger::debug("[Advanced Skin] SetupExtraTexture : Wetness texture path: {} for {}", wetnessTexturePath, foundPath ? foundPath : "(none)"); + + auto& workingExtraPtr = skinExtraTextures.try_emplace(hashKey).first->second; + workingExtraPtr.rfaosTexture = stateData.defaultTextureWhite; + workingExtraPtr.wetnessTexture = stateData.defaultTextureWhite; + workingExtraPtr.extraTexturePath = extraTexturePath; + workingExtraPtr.wetnessTexturePath = wetnessTexturePath; + + inTextureSet->SetTexturePath(RE::BSTextureSet::Texture::kEnvironment, workingExtraPtr.extraTexturePath.c_str()); + inTextureSet->SetTexturePath(RE::BSTextureSet::Texture::kMultilayer, workingExtraPtr.wetnessTexturePath.c_str()); + inTextureSet->SetTexture(RE::BSTextureSet::Texture::kEnvironment, workingExtraPtr.rfaosTexture); + inTextureSet->SetTexture(RE::BSTextureSet::Texture::kMultilayer, workingExtraPtr.wetnessTexture); + + workingExtraPtr.hasExtraTexture = workingExtraPtr.rfaosTexture != nullptr && !workingExtraPtr.extraTexturePath.empty() && workingExtraPtr.rfaosTexture != stateData.defaultTextureBlack; + workingExtraPtr.hasWetnessTexture = workingExtraPtr.wetnessTexture != nullptr && !workingExtraPtr.wetnessTexturePath.empty() && workingExtraPtr.wetnessTexture != stateData.defaultTextureBlack; + + if (workingExtraPtr.hasExtraTexture || workingExtraPtr.hasWetnessTexture) { + logger::debug("[Advanced Skin] SetupExtraTexture : Extra texture set with hash key: {}", hashKey); + } else { + logger::debug("[Advanced Skin] SetupExtraTexture : Failed to set extra texture for material: {}", hashKey); + } +} + +void Skin::BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material) +{ + auto materialFeature = material->GetFeature(); + if (materialFeature != RE::BSShaderMaterial::Feature::kFaceGen && + materialFeature != RE::BSShaderMaterial::Feature::kFaceGenRGBTint) { + return; + } + + auto materialTextureSet = material->textureSet.get(); + + uint32_t hashKey = 0; + hashKey = material->hashKey; + if (hashKey == 0) { + logger::error("[Advanced Skin] BSLightingShader_SetupMaterial : Invalid hash key for material: {}", static_cast(materialFeature)); + return; + } + + if (!skinExtraTextures.contains(hashKey)) { + // logger::debug("[Advanced Skin] BSLightingShader_SetupMaterial : Setting up extra texture for material: {}", static_cast(materialFeature)); + globals::features::skin.SetupExtraTexture(material, materialTextureSet, hashKey); + } + + auto graphicsState = globals::game::graphicsState; + const auto& workingExtraPtr = skinExtraTextures[hashKey]; + + if (workingExtraPtr.hasExtraTexture || workingExtraPtr.hasWetnessTexture) { + skinExtendedRendererState.SetExtraSkinPSTexture(workingExtraPtr.rfaosTexture->rendererTexture, workingExtraPtr.wetnessTexture->rendererTexture); + } else { + skinExtendedRendererState.SetExtraSkinPSTexture(graphicsState->GetRuntimeData().defaultTextureBlack->rendererTexture, graphicsState->GetRuntimeData().defaultTextureBlack->rendererTexture); + } +} + +void Skin::BSLightingShader_SetupGeometry(RE::BSRenderPass* a_pass) +{ + auto context = globals::d3d::context; + + if (settings.EnableSkin) { + auto geometry = a_pass->geometry; + float4 wetness = GetWetness(geometry); + + if (currentWetness != wetness) { + currentWetness = wetness; + PerGeometryData perGeometryData{}; + perGeometryData.skinPerGeometry = wetness; + PerGeometryCB->Update(perGeometryData); + } + + ID3D11Buffer* buffer = { PerGeometryCB->CB() }; + context->PSSetConstantBuffers(7, 1, &buffer); + } +} + +void Skin::SetShaderResources(ID3D11DeviceContext* a_context) +{ + if (skinExtendedRendererState.PSResourceModifiedBits != 0) { + a_context->PSSetShaderResources(71, 1, &skinExtendedRendererState.PSTexture.at(0)); + a_context->PSSetShaderResources(74, 1, &skinExtendedRendererState.PSTexture.at(1)); + } + skinExtendedRendererState.PSResourceModifiedBits = 0; +} + +void Skin::Hooks::BSLightingShader_SetupGeometry::thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags) +{ + auto& skin = globals::features::skin; + skin.BSLightingShader_SetupGeometry(Pass); + return func(This, Pass, RenderFlags); +} diff --git a/src/Features/Skin.h b/src/Features/Skin.h new file mode 100644 index 0000000000..ce023fa2a2 --- /dev/null +++ b/src/Features/Skin.h @@ -0,0 +1,145 @@ +#pragma once + +struct Skin : Feature +{ + static Skin* GetSingleton() + { + static Skin singleton; + return &singleton; + } + + virtual inline std::string GetName() override { return "Advanced Skin"; } + virtual inline std::string GetShortName() override { return "Skin"; } + virtual inline std::string_view GetShaderDefineName() override { return "CS_SKIN"; } + virtual std::string_view GetCategory() const override { return "Characters"; } + virtual std::pair> GetFeatureSummary() override + { + return { + "Advanced Skin enhances character skin rendering with multiple techniques.", + { "Physically-based dual specular lobes for realistic skin highlights", + "Tiled skin detail textures for enhanced realism", + "Extra textures support for roughness, translucency, and more", + "Reworked wetness system for dynamic skin effects" } + }; + } + virtual inline bool HasShaderDefine(RE::BSShader::Type t) override + { + return t == RE::BSShader::Type::Lighting; + }; + + virtual inline bool SupportsVR() { return true; } + + virtual void RestoreDefaultSettings() override; + virtual void DrawSettings() override; + + virtual void LoadSettings(json& o_json) override; + virtual void SaveSettings(json& o_json) override; + + virtual void Prepass() override; + virtual void PostPostLoad() override; + + virtual void SetupResources() override; + + void ReloadSkinDetail(); + void LoadSkinDetailTexture(); + + struct Settings + { + bool EnableSkin = true; + float SkinMainRoughness = 0.7f; + float SkinSecondRoughness = 0.35f; + float SkinSpecularTexMultiplier = 1.0f; + float SecondarySpecularStrength = 0.15f; + float F0 = 0.0278f; + float BaseColorMultiplier = 1.0f; + float PhysicalMainRoughnessMultiplier = 1.3f; + float PhysicalSecondRoughnessMultiplier = 0.75f; + float PhysicalSpecularStrength = 1.0f; + float ExtraEdgeRoughness = 0.25f; + bool EnableSkinDetail = true; + float SkinDetailStrength = 0.25f; + float SkinDetailTiling = 10.0f; + float BodyTilingMultiplier = 2.0f; + float ExtraSkinWetness = 0.0f; + float WetFadeTime = 10.0f; + float StartSweat = 0.75f; + float FullSweat = 0.15f; + float4 WetParams = { 512.0f, 0.7f, 10.0f, 4.0f }; + float Translucency = 0.1f; + float sssWidth = 0.2f; + bool UseSSS = true; + float FuzzStrength = 1.0f; + float FuzzRoughness = 0.35f; + float FuzzF0 = 0.045f; + bool UseDynamicWetness = false; + } settings; + + struct alignas(16) SkinData + { + float4 skinParams; + float4 skinParams2; + float4 skinDetailParams; + float4 sssParams; + float4 fuzzParams; + float4 physicalParams; + float4 wetParams; + }; + + struct alignas(16) PerGeometryData + { + float4 skinPerGeometry; + }; + + eastl::unique_ptr PerGeometryCB; + float4 currentWetness = { 0.0f, 0.0f, 0.0f, 0.0f }; + float playerStamina = 0.0f; + float playerStaminaMax = 0.0f; + + struct WaterHeightCacheEntry + { + uint frameCount = 0; + float waterHeight = 0.0f; + }; + std::unordered_map waterHeightCache; // keyed by actor formID + + struct ExtraTextures + { + RE::NiSourceTexturePtr rfaosTexture; + RE::NiSourceTexturePtr wetnessTexture; + std::string extraTexturePath; + std::string wetnessTexturePath; + bool hasExtraTexture = false; + bool hasWetnessTexture = false; + }; + + eastl::unique_ptr texSkinDetail = nullptr; + std::unordered_map skinExtraTextures; + std::unordered_map actorWetnessMap; // keyed by actor formID + + SkinData GetCommonBufferData(); + float GetWaterHeight(const RE::TESObjectREFR* a_ref, const RE::NiPoint3& a_pos); + float4 GetWetness(RE::BSGeometry* geometry); + + void SetupExtraTexture(RE::BSLightingShaderMaterialBase const* material, RE::BSTextureSet* inTextureSet, uint32_t i_hashKey); + void BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material); + void BSLightingShader_SetupGeometry(RE::BSRenderPass* a_pass); + void SetShaderResources(ID3D11DeviceContext* a_context); + + struct Hooks + { + struct BSLightingShader_SetupGeometry + { + static void thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags); + static inline REL::Relocation func; + }; + + static void Install() + { + stl::write_vfunc<0x6, BSLightingShader_SetupGeometry>(RE::VTABLE_BSLightingShader[0]); + logger::info("[Advanced Skin] Installed hooks"); + return; + } + }; + + bool isDynamicWetnessAvailable = false; +}; diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index 9ac26eea32..bcd09054e9 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -121,7 +121,7 @@ struct THExtendedRendererState } } thExtendedRendererState; -void TerrainHelper::SetShaderResouces(ID3D11DeviceContext* a_context) +void TerrainHelper::SetShaderResources(ID3D11DeviceContext* a_context) { uint32_t mask = thExtendedRendererState.PSResourceModifiedBits; diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index ed20870dd1..c83ee5d79e 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -43,7 +43,7 @@ struct TerrainHelper : Feature virtual bool SupportsVR() override { return true; }; virtual std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - void SetShaderResouces(ID3D11DeviceContext* a_context); + void SetShaderResources(ID3D11DeviceContext* a_context); bool TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land); void BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material); }; \ No newline at end of file diff --git a/src/Globals.cpp b/src/Globals.cpp index b388d46f4a..e1b60fc56b 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -21,6 +21,7 @@ #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/ScreenshotFeature.h" +#include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" @@ -89,6 +90,7 @@ namespace globals WeatherEditor weatherEditor{}; ExponentialHeightFog exponentialHeightFog{}; TruePBR truePBR{}; + Skin skin{}; namespace llf { diff --git a/src/Globals.h b/src/Globals.h index 342f92ce4f..326b9a34eb 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -37,6 +37,7 @@ struct WeatherEditor; struct ExponentialHeightFog; struct HDRDisplay; struct ScreenshotFeature; +struct Skin; class State; class Deferred; @@ -97,6 +98,7 @@ namespace globals extern WeatherEditor weatherEditor; extern ExponentialHeightFog exponentialHeightFog; extern TruePBR truePBR; + extern Skin skin; namespace llf { diff --git a/src/Hooks.cpp b/src/Hooks.cpp index a4d6edbb3a..6c9aa5d612 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -14,6 +14,7 @@ #include "Features/InteriorSun.h" #include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" +#include "Features/Skin.h" #include "Features/Upscaling.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" diff --git a/src/State.cpp b/src/State.cpp index 058bff6233..253efca58e 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -10,6 +10,7 @@ #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" +#include "Features/Skin.h" #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" @@ -53,6 +54,7 @@ void State::Draw() auto& terrainHelper = globals::features::terrainHelper; auto& cloudShadows = globals::features::cloudShadows; auto& weatherEditor = globals::features::weatherEditor; + auto& skin = globals::features::skin; auto& truePBR = globals::features::truePBR; auto context = globals::d3d::context; auto& volumetricShadows = globals::features::volumetricShadows; @@ -77,13 +79,18 @@ void State::Draw() } if (terrainHelper.loaded) { - ZoneScopedN("TerrainHelper::SetShaderResouces"); - terrainHelper.SetShaderResouces(context); + ZoneScopedN("TerrainHelper::SetShaderResources"); + terrainHelper.SetShaderResources(context); + } + + if (skin.loaded) { + ZoneScopedN("Skin::SetShaderResources"); + skin.SetShaderResources(context); } if (truePBR.loaded) { - ZoneScopedN("TruePBR::SetShaderResouces"); - truePBR.SetShaderResouces(context); + ZoneScopedN("TruePBR::SetShaderResources"); + truePBR.SetShaderResources(context); } if (permutationData != permutationDataPrevious) { diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 75be9fe75c..279fc86cd6 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -1529,7 +1529,7 @@ void TruePBR::SetupDefaultPBRLandTextureSet() } } -void TruePBR::SetShaderResouces(ID3D11DeviceContext* a_context) +void TruePBR::SetShaderResources(ID3D11DeviceContext* a_context) { uint32_t mask = extendedRendererState.PSResourceModifiedBits; diff --git a/src/TruePBR.h b/src/TruePBR.h index 3bf5d73e64..5607efbfe6 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -58,7 +58,7 @@ struct TruePBR : Feature bool TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land); bool BSLightingShader_SetupMaterial(RE::BSLightingShader* shader, RE::BSLightingShaderMaterialBase const* material); - void SetShaderResouces(ID3D11DeviceContext* a_context); + void SetShaderResources(ID3D11DeviceContext* a_context); virtual void GenerateShaderPermutations(RE::BSShader* shader) override; void SetupGlintsTexture(); From 2e57080e0a33734b1234d85c008974a6e9a2045c Mon Sep 17 00:00:00 2001 From: Dlizzio <77717521+Dlizzio@users.noreply.github.com> Date: Mon, 1 Jun 2026 04:22:36 -0700 Subject: [PATCH 08/55] chore: rename weather editor to CS editor (#2449) --- .../WeatherVariableRegistration.md | 4 +- features/{Weather Editor => CS Editor}/CORE | 0 .../CS Editor/Shaders/Features/CSEditor.ini | 2 + .../Shaders/Features/WeatherEditor.ini | 2 - .../EditorWindow.cpp | 26 ++--- .../EditorWindow.h | 0 .../InteriorOnlyPanel.cpp | 0 .../InteriorOnlyPanel.h | 2 +- .../LightEditor.cpp | 0 src/{WeatherEditor => CSEditor}/LightEditor.h | 0 .../PaletteWindow.cpp | 0 .../PaletteWindow.h | 0 .../Weather/CellLightingWidget.cpp | 0 .../Weather/CellLightingWidget.h | 0 .../Weather/ImageSpaceWidget.cpp | 0 .../Weather/ImageSpaceWidget.h | 0 .../Weather/LensFlareWidget.cpp | 0 .../Weather/LensFlareWidget.h | 0 .../Weather/LightingTemplateWidget.cpp | 0 .../Weather/LightingTemplateWidget.h | 0 .../Weather/PrecipitationWidget.cpp | 0 .../Weather/PrecipitationWidget.h | 0 .../Weather/ReferenceEffectWidget.cpp | 0 .../Weather/ReferenceEffectWidget.h | 0 .../Weather/SimpleFormWidget.h | 0 .../Weather/VolumetricLightingWidget.cpp | 0 .../Weather/VolumetricLightingWidget.h | 0 .../Weather/WeatherWidget.cpp | 0 .../Weather/WeatherWidget.h | 0 .../WeatherUtils.cpp | 0 .../WeatherUtils.h | 0 src/{WeatherEditor => CSEditor}/Widget.cpp | 0 src/{WeatherEditor => CSEditor}/Widget.h | 0 src/Deferred.cpp | 2 +- src/Feature.cpp | 4 +- .../{WeatherEditor.cpp => CSEditor.cpp} | 104 +++++++++--------- src/Features/{WeatherEditor.h => CSEditor.h} | 16 +-- src/Features/InverseSquareLighting.cpp | 2 +- src/Features/WetnessEffects.cpp | 12 +- src/Globals.cpp | 4 +- src/Globals.h | 4 +- src/Menu.cpp | 40 +++---- src/Menu.h | 16 +-- src/Menu/BackgroundBlur.cpp | 16 +-- src/Menu/BackgroundBlur.h | 6 +- src/Menu/HomePageRenderer.cpp | 8 +- src/Menu/OverlayRenderer.cpp | 2 +- src/Menu/SettingsTabRenderer.cpp | 10 +- src/Menu/SettingsTabRenderer.h | 4 +- src/State.cpp | 6 +- src/Utils/UI.cpp | 10 +- 51 files changed, 151 insertions(+), 151 deletions(-) rename features/{Weather Editor => CS Editor}/CORE (100%) create mode 100644 features/CS Editor/Shaders/Features/CSEditor.ini delete mode 100644 features/Weather Editor/Shaders/Features/WeatherEditor.ini rename src/{WeatherEditor => CSEditor}/EditorWindow.cpp (99%) rename src/{WeatherEditor => CSEditor}/EditorWindow.h (100%) rename src/{WeatherEditor => CSEditor}/InteriorOnlyPanel.cpp (100%) rename src/{WeatherEditor => CSEditor}/InteriorOnlyPanel.h (96%) rename src/{WeatherEditor => CSEditor}/LightEditor.cpp (100%) rename src/{WeatherEditor => CSEditor}/LightEditor.h (100%) rename src/{WeatherEditor => CSEditor}/PaletteWindow.cpp (100%) rename src/{WeatherEditor => CSEditor}/PaletteWindow.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/CellLightingWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/CellLightingWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/ImageSpaceWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/ImageSpaceWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/LensFlareWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/LensFlareWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/LightingTemplateWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/LightingTemplateWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/PrecipitationWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/PrecipitationWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/ReferenceEffectWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/ReferenceEffectWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/SimpleFormWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/VolumetricLightingWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/VolumetricLightingWidget.h (100%) rename src/{WeatherEditor => CSEditor}/Weather/WeatherWidget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Weather/WeatherWidget.h (100%) rename src/{WeatherEditor => CSEditor}/WeatherUtils.cpp (100%) rename src/{WeatherEditor => CSEditor}/WeatherUtils.h (100%) rename src/{WeatherEditor => CSEditor}/Widget.cpp (100%) rename src/{WeatherEditor => CSEditor}/Widget.h (100%) rename src/Features/{WeatherEditor.cpp => CSEditor.cpp} (92%) rename src/Features/{WeatherEditor.h => CSEditor.h} (93%) diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index afc6ca519a..6b1fe80ae2 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -135,7 +135,7 @@ The system now automatically: - Saves/loads weather-specific settings to JSON - Interpolates variables during weather transitions -- Appears in the weather editor UI with per-weather toggle buttons +- Appears in the CS Editor UI with per-weather toggle buttons - Handles default values and missing data - Shows weather-controlled status in feature settings UI @@ -278,7 +278,7 @@ Weather-specific settings are stored in: ``` Data/SKSE/Plugins/CommunityShaders/Weathers/ - WeatherEditorID_FormID.json + WeatherFormEditorID_FormID.json ``` Each file contains settings for all features: diff --git a/features/Weather Editor/CORE b/features/CS Editor/CORE similarity index 100% rename from features/Weather Editor/CORE rename to features/CS Editor/CORE diff --git a/features/CS Editor/Shaders/Features/CSEditor.ini b/features/CS Editor/Shaders/Features/CSEditor.ini new file mode 100644 index 0000000000..629d28c0f7 --- /dev/null +++ b/features/CS Editor/Shaders/Features/CSEditor.ini @@ -0,0 +1,2 @@ +[Info] +Version = 2-0-2 diff --git a/features/Weather Editor/Shaders/Features/WeatherEditor.ini b/features/Weather Editor/Shaders/Features/WeatherEditor.ini deleted file mode 100644 index 9a577382c4..0000000000 --- a/features/Weather Editor/Shaders/Features/WeatherEditor.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Info] -Version = 2-0-1 diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/CSEditor/EditorWindow.cpp similarity index 99% rename from src/WeatherEditor/EditorWindow.cpp rename to src/CSEditor/EditorWindow.cpp index 9d31591f82..30077298f4 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/CSEditor/EditorWindow.cpp @@ -2,7 +2,7 @@ #include "Features/HDRDisplay.h" #include "Features/Upscaling.h" -#include "Features/WeatherEditor.h" +#include "Features/CSEditor.h" #include "Globals.h" #include "InteriorOnlyPanel.h" #include "Menu.h" @@ -180,7 +180,7 @@ std::string EditorWindow::ResolveEditorId(RE::TESForm* form, const WidgetVec& wi void EditorWindow::ShowObjectsWindow() { - Util::BeginWithRoundedClose("Weather and Lighting Browser", nullptr); + Util::BeginWithRoundedClose("CS Editor Browser", nullptr); // Reset filter state when the user switches categories so stale column // selections (e.g. Status) don't hide all items in the new category. @@ -215,7 +215,7 @@ void EditorWindow::ShowObjectsWindow() // List of categories const char* categories[] = { "Weather", "ImageSpace", "Lighting Template", "Cell Lighting", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect", - "Interior Only", "Lighting editor" }; + "Interior Only", "Light Editor" }; for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { // Highlight the selected category if (ImGui::Selectable(categories[i], m_selectedCategory == categories[i])) { @@ -240,7 +240,7 @@ void EditorWindow::ShowObjectsWindow() return; } - if (m_selectedCategory == "Lighting editor") { + if (m_selectedCategory == "Light Editor") { BeginScrollableContent("##LightEditorScroll"); lightEditor.DrawSettings(); EndScrollableContent(); @@ -1006,7 +1006,7 @@ void EditorWindow::RenderUI() if (hdrActive) ImGui::BeginDisabled(); if (ImGui::Checkbox("Viewport", &settings.showViewport)) { - BackgroundBlur::SetWeatherEditorActive(settings.showViewport); + BackgroundBlur::SetCSEditorActive(settings.showViewport); Save(); } if (hdrActive) { @@ -1039,7 +1039,7 @@ void EditorWindow::RenderUI() ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { - ImGui::Text("Weather Editor"); + ImGui::Text("CS Editor"); ImGui::Separator(); ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Keyboard Shortcuts:"); ImGui::BulletText("Ctrl+F: Focus search"); @@ -1152,7 +1152,7 @@ void EditorWindow::RenderUI() char previewStatusBuf[128] = {}; bool showPreviewStatus = previewMode != PreviewMode::None; if (showPreviewStatus) { - std::string hotkey = Util::Input::KeyIdToString(menu->GetSettings().WeatherEditorToggleKey); + std::string hotkey = Util::Input::KeyIdToString(menu->GetSettings().CSEditorToggleKey); if (previewMode == PreviewMode::FreeCamera) std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), " [ %s ] FREE CAMERA (Speed: %.0f)", hotkey.c_str(), flySpeed); else if (previewMode == PreviewMode::FreeCameraLocked) @@ -1264,7 +1264,7 @@ void EditorWindow::RenderUI() ImGui::SetCursorScreenPos(ImVec2(xButtonX, cursorY)); if (Util::ErrorButton("X", ImVec2(closeButtonSize, closeButtonSize))) open = false; - Util::AddTooltip("Close Weather Editor (Esc)"); + Util::AddTooltip("Close CS Editor (Esc)"); ImGui::PopClipRect(); // End bottom-border clip rect @@ -1411,13 +1411,13 @@ void EditorWindow::UpdateOpenState() if (open && !wasOpen) { DisableVanityCamera(); HideGameMenus(); - BackgroundBlur::SetWeatherEditorActive(IsViewportActive()); + BackgroundBlur::SetCSEditorActive(IsViewportActive()); } else if (!open && wasOpen) { lightEditor.ResetOverrides(); RestoreVanityCamera(); ShowGameMenus(); - BackgroundBlur::SetWeatherEditorActive(false); + BackgroundBlur::SetCSEditorActive(false); } wasOpen = open; @@ -1433,7 +1433,7 @@ void EditorWindow::Draw() static bool prevViewportActive = false; const bool viewportActive = IsViewportActive(); if (viewportActive != prevViewportActive) { - BackgroundBlur::SetWeatherEditorActive(viewportActive); + BackgroundBlur::SetCSEditorActive(viewportActive); prevViewportActive = viewportActive; } } @@ -1959,7 +1959,7 @@ void EditorWindow::HideGameMenus() if (auto ui = RE::UI::GetSingleton()) { ui->ShowMenus(false); gameMenusHidden = true; - logger::info("Game menus hidden for weather editor"); + logger::info("Game menus hidden for CS editor"); } } @@ -1971,7 +1971,7 @@ void EditorWindow::ShowGameMenus() if (auto ui = RE::UI::GetSingleton()) { ui->ShowMenus(true); gameMenusHidden = false; - logger::info("Game menus restored after weather editor"); + logger::info("Game menus restored after CS editor"); } } diff --git a/src/WeatherEditor/EditorWindow.h b/src/CSEditor/EditorWindow.h similarity index 100% rename from src/WeatherEditor/EditorWindow.h rename to src/CSEditor/EditorWindow.h diff --git a/src/WeatherEditor/InteriorOnlyPanel.cpp b/src/CSEditor/InteriorOnlyPanel.cpp similarity index 100% rename from src/WeatherEditor/InteriorOnlyPanel.cpp rename to src/CSEditor/InteriorOnlyPanel.cpp diff --git a/src/WeatherEditor/InteriorOnlyPanel.h b/src/CSEditor/InteriorOnlyPanel.h similarity index 96% rename from src/WeatherEditor/InteriorOnlyPanel.h rename to src/CSEditor/InteriorOnlyPanel.h index a282feb358..5d087f5ceb 100644 --- a/src/WeatherEditor/InteriorOnlyPanel.h +++ b/src/CSEditor/InteriorOnlyPanel.h @@ -2,7 +2,7 @@ #include "Utils/UI.h" -/// UI panel for managing Interior Only scene settings within the Weather Editor. +/// UI panel for managing Interior Only scene settings within the CS Editor. /// Renders the list of entries with add/pause/delete controls. namespace InteriorOnlyPanel { diff --git a/src/WeatherEditor/LightEditor.cpp b/src/CSEditor/LightEditor.cpp similarity index 100% rename from src/WeatherEditor/LightEditor.cpp rename to src/CSEditor/LightEditor.cpp diff --git a/src/WeatherEditor/LightEditor.h b/src/CSEditor/LightEditor.h similarity index 100% rename from src/WeatherEditor/LightEditor.h rename to src/CSEditor/LightEditor.h diff --git a/src/WeatherEditor/PaletteWindow.cpp b/src/CSEditor/PaletteWindow.cpp similarity index 100% rename from src/WeatherEditor/PaletteWindow.cpp rename to src/CSEditor/PaletteWindow.cpp diff --git a/src/WeatherEditor/PaletteWindow.h b/src/CSEditor/PaletteWindow.h similarity index 100% rename from src/WeatherEditor/PaletteWindow.h rename to src/CSEditor/PaletteWindow.h diff --git a/src/WeatherEditor/Weather/CellLightingWidget.cpp b/src/CSEditor/Weather/CellLightingWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/CellLightingWidget.cpp rename to src/CSEditor/Weather/CellLightingWidget.cpp diff --git a/src/WeatherEditor/Weather/CellLightingWidget.h b/src/CSEditor/Weather/CellLightingWidget.h similarity index 100% rename from src/WeatherEditor/Weather/CellLightingWidget.h rename to src/CSEditor/Weather/CellLightingWidget.h diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/CSEditor/Weather/ImageSpaceWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/ImageSpaceWidget.cpp rename to src/CSEditor/Weather/ImageSpaceWidget.cpp diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/CSEditor/Weather/ImageSpaceWidget.h similarity index 100% rename from src/WeatherEditor/Weather/ImageSpaceWidget.h rename to src/CSEditor/Weather/ImageSpaceWidget.h diff --git a/src/WeatherEditor/Weather/LensFlareWidget.cpp b/src/CSEditor/Weather/LensFlareWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/LensFlareWidget.cpp rename to src/CSEditor/Weather/LensFlareWidget.cpp diff --git a/src/WeatherEditor/Weather/LensFlareWidget.h b/src/CSEditor/Weather/LensFlareWidget.h similarity index 100% rename from src/WeatherEditor/Weather/LensFlareWidget.h rename to src/CSEditor/Weather/LensFlareWidget.h diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/CSEditor/Weather/LightingTemplateWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/LightingTemplateWidget.cpp rename to src/CSEditor/Weather/LightingTemplateWidget.cpp diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.h b/src/CSEditor/Weather/LightingTemplateWidget.h similarity index 100% rename from src/WeatherEditor/Weather/LightingTemplateWidget.h rename to src/CSEditor/Weather/LightingTemplateWidget.h diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.cpp b/src/CSEditor/Weather/PrecipitationWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/PrecipitationWidget.cpp rename to src/CSEditor/Weather/PrecipitationWidget.cpp diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.h b/src/CSEditor/Weather/PrecipitationWidget.h similarity index 100% rename from src/WeatherEditor/Weather/PrecipitationWidget.h rename to src/CSEditor/Weather/PrecipitationWidget.h diff --git a/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp b/src/CSEditor/Weather/ReferenceEffectWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/ReferenceEffectWidget.cpp rename to src/CSEditor/Weather/ReferenceEffectWidget.cpp diff --git a/src/WeatherEditor/Weather/ReferenceEffectWidget.h b/src/CSEditor/Weather/ReferenceEffectWidget.h similarity index 100% rename from src/WeatherEditor/Weather/ReferenceEffectWidget.h rename to src/CSEditor/Weather/ReferenceEffectWidget.h diff --git a/src/WeatherEditor/Weather/SimpleFormWidget.h b/src/CSEditor/Weather/SimpleFormWidget.h similarity index 100% rename from src/WeatherEditor/Weather/SimpleFormWidget.h rename to src/CSEditor/Weather/SimpleFormWidget.h diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp b/src/CSEditor/Weather/VolumetricLightingWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/VolumetricLightingWidget.cpp rename to src/CSEditor/Weather/VolumetricLightingWidget.cpp diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.h b/src/CSEditor/Weather/VolumetricLightingWidget.h similarity index 100% rename from src/WeatherEditor/Weather/VolumetricLightingWidget.h rename to src/CSEditor/Weather/VolumetricLightingWidget.h diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/CSEditor/Weather/WeatherWidget.cpp similarity index 100% rename from src/WeatherEditor/Weather/WeatherWidget.cpp rename to src/CSEditor/Weather/WeatherWidget.cpp diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/CSEditor/Weather/WeatherWidget.h similarity index 100% rename from src/WeatherEditor/Weather/WeatherWidget.h rename to src/CSEditor/Weather/WeatherWidget.h diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/CSEditor/WeatherUtils.cpp similarity index 100% rename from src/WeatherEditor/WeatherUtils.cpp rename to src/CSEditor/WeatherUtils.cpp diff --git a/src/WeatherEditor/WeatherUtils.h b/src/CSEditor/WeatherUtils.h similarity index 100% rename from src/WeatherEditor/WeatherUtils.h rename to src/CSEditor/WeatherUtils.h diff --git a/src/WeatherEditor/Widget.cpp b/src/CSEditor/Widget.cpp similarity index 100% rename from src/WeatherEditor/Widget.cpp rename to src/CSEditor/Widget.cpp diff --git a/src/WeatherEditor/Widget.h b/src/CSEditor/Widget.h similarity index 100% rename from src/WeatherEditor/Widget.h rename to src/CSEditor/Widget.h diff --git a/src/Deferred.cpp b/src/Deferred.cpp index cecfb90483..62146505f6 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -14,7 +14,7 @@ #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" #include "Features/VR.h" -#include "Features/WeatherEditor.h" +#include "Features/CSEditor.h" #include "Hooks.h" diff --git a/src/Feature.cpp b/src/Feature.cpp index b5e7287a79..9cfad1278e 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -36,7 +36,7 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/WeatherEditor.h" +#include "Features/CSEditor.h" #include "Features/WetnessEffects.h" #include "Menu.h" #include "SettingsOverrideManager.h" @@ -241,7 +241,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::extendedTranslucency, &globals::features::upscaling, &globals::features::renderDoc, - &globals::features::weatherEditor, + &globals::features::csEditor, &globals::features::screenshotFeature, &globals::features::linearLighting, &globals::features::unifiedWater, diff --git a/src/Features/WeatherEditor.cpp b/src/Features/CSEditor.cpp similarity index 92% rename from src/Features/WeatherEditor.cpp rename to src/Features/CSEditor.cpp index 8f6ff39e43..7ff0c15f2f 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -1,4 +1,4 @@ -#include "WeatherEditor.h" +#include "CSEditor.h" #include "Deferred.h" #include "Feature.h" @@ -9,7 +9,7 @@ #include "Utils/UI.h" #include "WeatherManager.h" -#include "WeatherEditor/EditorWindow.h" +#include "CSEditor/EditorWindow.h" #include #include #include @@ -20,18 +20,18 @@ namespace } NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( - WeatherEditor::WeatherDetailsWindowSettings, + CSEditor::WeatherDetailsWindowSettings, Enabled, ShowInOverlay, Position, PositionSet) -void WeatherEditor::DataLoaded() +void CSEditor::DataLoaded() { s_dataAvailable = true; } -bool WeatherEditor::HasWidgetJsonFiles() +bool CSEditor::HasWidgetJsonFiles() { if (s_checkedWidgetJsonFiles) return s_hasWidgetJsonFiles; @@ -42,7 +42,7 @@ bool WeatherEditor::HasWidgetJsonFiles() std::error_code ec; const bool isDirectory = std::filesystem::is_directory(widgetSettingsPath, ec); if (ec) { - logger::warn("[WeatherEditor] Failed to inspect widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); + logger::warn("[CSEditor] Failed to inspect widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); continue; } if (!isDirectory) @@ -52,7 +52,7 @@ bool WeatherEditor::HasWidgetJsonFiles() std::error_code entryEc; const bool isRegularFile = it->is_regular_file(entryEc); if (entryEc) { - logger::warn("[WeatherEditor] Failed to inspect widget settings file '{}': {}", it->path().string(), entryEc.message()); + logger::warn("[CSEditor] Failed to inspect widget settings file '{}': {}", it->path().string(), entryEc.message()); continue; } if (isRegularFile && _stricmp(it->path().extension().string().c_str(), kJsonExtension) == 0) { @@ -62,7 +62,7 @@ bool WeatherEditor::HasWidgetJsonFiles() } } if (ec) { - logger::warn("[WeatherEditor] Failed to scan widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); + logger::warn("[CSEditor] Failed to scan widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); continue; } } @@ -71,12 +71,12 @@ bool WeatherEditor::HasWidgetJsonFiles() return false; } -bool WeatherEditor::ShouldPreloadEditorResources() +bool CSEditor::ShouldPreloadEditorResources() { return s_dataAvailable && !s_resourcesInitialized && EditorWindow::CanBeOpen() && HasWidgetJsonFiles(); } -void WeatherEditor::EnsureWeatherListLoaded() +void CSEditor::EnsureWeatherListLoaded() { if (!s_dataAvailable) return; @@ -84,7 +84,7 @@ void WeatherEditor::EnsureWeatherListLoaded() LoadAllWeathers(); } -void WeatherEditor::EnsureDataLoaded() +void CSEditor::EnsureDataLoaded() { if (!s_dataAvailable) return; @@ -96,7 +96,7 @@ void WeatherEditor::EnsureDataLoaded() LoadAllWeathers(); } -void WeatherEditor::OpenEditorWindow() +void CSEditor::OpenEditorWindow() { if (!EditorWindow::CanBeOpen()) return; @@ -105,7 +105,7 @@ void WeatherEditor::OpenEditorWindow() EditorWindow::GetSingleton()->open = true; } -void WeatherEditor::ToggleEditorWindow() +void CSEditor::ToggleEditorWindow() { auto* editorWindow = EditorWindow::GetSingleton(); if (!editorWindow) @@ -154,26 +154,26 @@ void LerpDirectional(RE::BGSDirectionalAmbientLightingColors::Directional& oldCo LerpColor(oldColor.z.min, newColor.z.min, changePct); } -void WeatherEditor::DrawSettings() +void CSEditor::DrawSettings() { EnsureWeatherListLoaded(); bool canOpen = EditorWindow::CanBeOpen(); ImGui::BeginDisabled(!canOpen); - if (ImGui::Button("Open Editor", { -1, 0 })) + if (ImGui::Button("Open CS Editor", { -1, 0 })) OpenEditorWindow(); ImGui::EndDisabled(); // Time controls DrawTimeControls(); - // Basic weather editor info + // Basic CS editor info DrawWeatherStatusPanel(); // Integrated Weather Picker UI DrawWeatherPickerSection(); } -void WeatherEditor::Prepass() +void CSEditor::Prepass() { if (ShouldPreloadEditorResources()) { EnsureDataLoaded(); @@ -193,7 +193,7 @@ void WeatherEditor::Prepass() editorWindow->UpdateTimeState(); } -void WeatherEditor::DrawWeatherPickerSection() +void CSEditor::DrawWeatherPickerSection() { ImGui::Spacing(); Util::DrawSectionHeader("Weather Details"); @@ -221,7 +221,7 @@ void WeatherEditor::DrawWeatherPickerSection() RenderFeatureWeatherAnalysis(); } -void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) +void CSEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) { if (!oldWeather || !newWeather) { // Avoid dereferencing null pointers; nothing to lerp. @@ -294,7 +294,7 @@ void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newW } } -void WeatherEditor::DrawTimeControls() +void CSEditor::DrawTimeControls() { ImGui::Spacing(); Util::DrawSectionHeader("Time Controls"); @@ -303,7 +303,7 @@ void WeatherEditor::DrawTimeControls() ImGui::Spacing(); } -void WeatherEditor::DrawWeatherStatusPanel() +void CSEditor::DrawWeatherStatusPanel() { ImGui::Spacing(); Util::DrawSectionHeader("Weather Status"); @@ -364,7 +364,7 @@ void WeatherEditor::DrawWeatherStatusPanel() // Weather Picker functionality (integrated from WeatherPicker feature) // ================================================================================ -void WeatherEditor::RenderWeatherDetailsWindow(bool* open) +void CSEditor::RenderWeatherDetailsWindow(bool* open) { if (!open || !*open) return; @@ -406,7 +406,7 @@ void WeatherEditor::RenderWeatherDetailsWindow(bool* open) ImGui::End(); } -ImVec4 WeatherEditor::GetWeatherTypeColor(RE::TESWeather* weather) +ImVec4 CSEditor::GetWeatherTypeColor(RE::TESWeather* weather) { if (!weather) { return Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor; @@ -440,7 +440,7 @@ ImVec4 WeatherEditor::GetWeatherTypeColor(RE::TESWeather* weather) } // --- Helper: Display basic weather info (name, flags, percentage) --- -void WeatherEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct) +void CSEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct) { if (!weather) { ImGui::BulletText("No Weather Found"); @@ -449,13 +449,13 @@ void WeatherEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weath std::string weatherText = Util::FormatWeather(weather); ImGui::Bullet(); ImGui::SameLine(); - bool showTooltip = WeatherEditor::RenderMultiColorWeatherName(weather, weatherText); + bool showTooltip = CSEditor::RenderMultiColorWeatherName(weather, weatherText); if (showTooltip) { ImGui::BeginTooltip(); ImGui::Text("Name: %s", weather->GetName() ? weather->GetName() : "Unnamed"); ImGui::Text("Editor ID: %s", weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); ImGui::Text("Form ID: 0x%08X", weather->GetFormID()); - auto flagNames = WeatherEditor::GetWeatherFlagNames(weather); + auto flagNames = CSEditor::GetWeatherFlagNames(weather); if (!flagNames.empty()) { std::string joinedFlags = flagNames[0]; for (size_t j = 1; j < flagNames.size(); ++j) { @@ -472,7 +472,7 @@ void WeatherEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weath } } -void WeatherEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) +void CSEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) { if (!weather || !weather->precipitationData) { ImGui::BulletText("Particle Density: No precipitation data"); @@ -500,7 +500,7 @@ void WeatherEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) } } -void WeatherEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiveElements) +void CSEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiveElements) { if (!weather || (uint8_t)weather->data.thunderLightningFrequency == 0) return; @@ -554,7 +554,7 @@ void WeatherEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInter } } -void WeatherEditor::DisplayWindInfo(RE::TESWeather* weather) +void CSEditor::DisplayWindInfo(RE::TESWeather* weather) { auto sky = globals::game::sky; if (!weather || (weather->data.windSpeed <= 0 && (!sky || sky->windSpeed <= 0.0f))) @@ -616,15 +616,15 @@ void WeatherEditor::DisplayWindInfo(RE::TESWeather* weather) } // --- Main function: now just delegates to helpers --- -void WeatherEditor::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, bool showInteractiveElements) +void CSEditor::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, bool showInteractiveElements) { - WeatherEditor::DisplayWeatherBasicInfo(weather, weatherPct); - WeatherEditor::DisplayPrecipitationInfo(weather); - WeatherEditor::DisplayLightningInfo(weather, showInteractiveElements); - WeatherEditor::DisplayWindInfo(weather); + CSEditor::DisplayWeatherBasicInfo(weather, weatherPct); + CSEditor::DisplayPrecipitationInfo(weather); + CSEditor::DisplayLightningInfo(weather, showInteractiveElements); + CSEditor::DisplayWindInfo(weather); } -void WeatherEditor::RenderWeatherControls(RE::Sky* sky) +void CSEditor::RenderWeatherControls(RE::Sky* sky) { // Weather Selection Section (only show interactive elements in inline mode) static bool weatherControlsExpanded = true; @@ -706,7 +706,7 @@ void WeatherEditor::RenderWeatherControls(RE::Sky* sky) sky->ResetWeather(); // Update the selection box to reflect the reset weather without double-applying s_selectedWeatherIdx = FindWeatherIndex(sky->defaultWeather); - logger::info("[WeatherEditor] Reset weather to default"); + logger::info("[CSEditor] Reset weather to default"); } if (auto _tt = Util::HoverTooltipWrapper()) { @@ -788,7 +788,7 @@ void WeatherEditor::RenderWeatherControls(RE::Sky* sky) editorWindow->LockWeather(selectedWeather); Util::ClearComboSearch(kWeatherSearchId); - logger::info("[WeatherEditor] Changed weather to: {}", Util::FormatWeather(selectedWeather)); + logger::info("[CSEditor] Changed weather to: {}", Util::FormatWeather(selectedWeather)); break; } @@ -809,7 +809,7 @@ void WeatherEditor::RenderWeatherControls(RE::Sky* sky) } } -void WeatherEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) +void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) { ImGui::Spacing(); ImGui::Spacing(); @@ -845,7 +845,7 @@ void WeatherEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInter } } -void WeatherEditor::RenderCoreWeatherDetails(bool showInteractiveElements) +void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements) { const auto showError = [](const char* msg) { auto menu = Menu::GetSingleton(); @@ -868,7 +868,7 @@ void WeatherEditor::RenderCoreWeatherDetails(bool showInteractiveElements) } } -void WeatherEditor::LoadAllWeathers() +void CSEditor::LoadAllWeathers() { if (s_weathersLoaded) return; @@ -892,7 +892,7 @@ void WeatherEditor::LoadAllWeathers() } } -void WeatherEditor::UpdateFilteredWeathers() +void CSEditor::UpdateFilteredWeathers() { s_filteredWeathers.clear(); for (auto weather : s_allWeathers) { @@ -931,7 +931,7 @@ void WeatherEditor::UpdateFilteredWeathers() } } -int WeatherEditor::FindWeatherIndex(RE::TESWeather* targetWeather) +int CSEditor::FindWeatherIndex(RE::TESWeather* targetWeather) { if (!targetWeather) return -1; @@ -943,13 +943,13 @@ int WeatherEditor::FindWeatherIndex(RE::TESWeather* targetWeather) return -1; } -void WeatherEditor::RenderFeatureWeatherAnalysis() +void CSEditor::RenderFeatureWeatherAnalysis() { // Iterate through all loaded features to show their weather analysis for (auto* feature : Feature::GetFeatureList()) { if (feature->loaded) { - // Skip the WeatherEditor itself to avoid recursion - if (feature == &globals::features::weatherEditor) { + // Skip the CSEditor itself to avoid recursion + if (feature == &globals::features::csEditor) { continue; } @@ -980,7 +980,7 @@ void WeatherEditor::RenderFeatureWeatherAnalysis() } } -std::vector WeatherEditor::GetWeatherFlagNames(RE::TESWeather* weather) +std::vector CSEditor::GetWeatherFlagNames(RE::TESWeather* weather) { std::vector flagNames; if (!weather) { @@ -1032,7 +1032,7 @@ std::vector WeatherEditor::GetWeatherFlagNames(RE::TESWeather* weat return flagNames; } -bool WeatherEditor::RenderMultiColorWeatherName(RE::TESWeather* weather, const std::string& weatherName) +bool CSEditor::RenderMultiColorWeatherName(RE::TESWeather* weather, const std::string& weatherName) { if (!weather) { ImGui::Text("%s", weatherName.c_str()); @@ -1094,7 +1094,7 @@ bool WeatherEditor::RenderMultiColorWeatherName(RE::TESWeather* weather, const s } // Helper function to get color for a specific weather flag -ImVec4 WeatherEditor::GetWeatherFlagColor(RE::TESWeather::WeatherDataFlag flag) +ImVec4 CSEditor::GetWeatherFlagColor(RE::TESWeather::WeatherDataFlag flag) { const auto& theme = Menu::GetSingleton()->GetTheme(); @@ -1117,7 +1117,7 @@ ImVec4 WeatherEditor::GetWeatherFlagColor(RE::TESWeather::WeatherDataFlag flag) } // Helper function to get color for a specific flag name -ImVec4 WeatherEditor::GetWeatherFlagColorByName(const std::string& flagName) +ImVec4 CSEditor::GetWeatherFlagColorByName(const std::string& flagName) { // Map display flag names back to enum values // Note: We use manual mapping here because the display names (from GetWeatherFlagNames) @@ -1140,7 +1140,7 @@ ImVec4 WeatherEditor::GetWeatherFlagColorByName(const std::string& flagName) return Menu::GetSingleton()->GetTheme().StatusPalette.Warning; } -std::string WeatherEditor::GetDisplayName(const RE::TESWeather* weather) +std::string CSEditor::GetDisplayName(const RE::TESWeather* weather) { if (!weather) { return "Unknown"; @@ -1156,7 +1156,7 @@ std::string WeatherEditor::GetDisplayName(const RE::TESWeather* weather) return std::to_string(weather->GetFormID()); } -void WeatherEditor::DrawOverlay() +void CSEditor::DrawOverlay() { auto player = RE::PlayerCharacter::GetSingleton(); if (!player || !player->parentCell) @@ -1175,7 +1175,7 @@ void WeatherEditor::DrawOverlay() s_prevOverlayVisible = overlayVisible; } -bool WeatherEditor::IsOverlayVisible() const +bool CSEditor::IsOverlayVisible() const { return WeatherDetailsWindow.ShowInOverlay; } diff --git a/src/Features/WeatherEditor.h b/src/Features/CSEditor.h similarity index 93% rename from src/Features/WeatherEditor.h rename to src/Features/CSEditor.h index a09f1b25fe..0948a13c6b 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/CSEditor.h @@ -5,18 +5,18 @@ #include "OverlayFeature.h" #include "State.h" -struct WeatherEditor : OverlayFeature +struct CSEditor : OverlayFeature { public: - static WeatherEditor* GetSingleton() + static CSEditor* GetSingleton() { - static WeatherEditor singleton; + static CSEditor singleton; return &singleton; } - virtual inline std::string GetName() override { return "Weather Editor"; } - virtual inline std::string GetShortName() override { return "WeatherEditor"; } - virtual inline std::string_view GetShaderDefineName() override { return "WEATHER"; } + virtual inline std::string GetName() override { return "CS Editor"; } + virtual inline std::string GetShortName() override { return "CSEditor"; } + virtual inline std::string_view GetShaderDefineName() override { return "CS_EDITOR"; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kUtility; } virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } @@ -25,7 +25,7 @@ struct WeatherEditor : OverlayFeature virtual inline std::pair> GetFeatureSummary() override { return { - "Development tool for editing weather, testing weather transitions, and managing weather-related feature settings.", + "Development tool for inspecting, editing, and previewing renderer-facing data in-game.", { "Provides weather editing functionality", "Includes dynamic saving and loading of vanilla post processing and weather settings.", "Real-time editing and previewing of effects", @@ -158,7 +158,7 @@ struct WeatherEditor : OverlayFeature { bool operator()(const RE::TESWeather* a, const RE::TESWeather* b) const { - return WeatherEditor::GetDisplayName(a) < WeatherEditor::GetDisplayName(b); + return CSEditor::GetDisplayName(a) < CSEditor::GetDisplayName(b); } }; diff --git a/src/Features/InverseSquareLighting.cpp b/src/Features/InverseSquareLighting.cpp index 03e830456d..bf70373120 100644 --- a/src/Features/InverseSquareLighting.cpp +++ b/src/Features/InverseSquareLighting.cpp @@ -1,7 +1,7 @@ #include "InverseSquareLighting.h" #include "Features/InverseSquareLighting/Common.h" #include "LightLimitFix.h" -#include "WeatherEditor/EditorWindow.h" +#include "CSEditor/EditorWindow.h" #include void InverseSquareLighting::PostPostLoad() diff --git a/src/Features/WetnessEffects.cpp b/src/Features/WetnessEffects.cpp index dd077abf04..15025d78ad 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -1,6 +1,6 @@ #include "WetnessEffects.h" #include "Menu.h" -#include "WeatherEditor.h" +#include "CSEditor.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( WetnessEffects::Settings, @@ -477,14 +477,14 @@ void WetnessEffects::DrawSettings() ImGui::Spacing(); ImGui::Spacing(); - auto& weatherEditor = globals::features::weatherEditor; - if (weatherEditor.loaded) { - if (ImGui::SmallButton(("Open " + weatherEditor.GetName()).c_str())) { + auto& csEditor = globals::features::csEditor; + if (csEditor.loaded) { + if (ImGui::SmallButton(("Open " + csEditor.GetName()).c_str())) { // Navigate to the replacement feature in the menu - Menu::GetSingleton()->SelectFeatureMenu(weatherEditor.GetShortName()); + Menu::GetSingleton()->SelectFeatureMenu(csEditor.GetShortName()); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Open the installed %s feature", weatherEditor.GetShortName().c_str()); + ImGui::Text("Open the installed %s feature", csEditor.GetShortName().c_str()); } } diff --git a/src/Globals.cpp b/src/Globals.cpp index e1b60fc56b..15cba11777 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -35,7 +35,7 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/WeatherEditor.h" +#include "Features/CSEditor.h" #include "Features/WetnessEffects.h" #include "Menu.h" #include "ShaderCache.h" @@ -87,7 +87,7 @@ namespace globals HDRDisplay hdrDisplay{}; RenderDoc renderDoc{}; ScreenshotFeature screenshotFeature{}; - WeatherEditor weatherEditor{}; + CSEditor csEditor{}; ExponentialHeightFog exponentialHeightFog{}; TruePBR truePBR{}; Skin skin{}; diff --git a/src/Globals.h b/src/Globals.h index 326b9a34eb..9fd5bb081c 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -33,7 +33,7 @@ struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; class Profiler; -struct WeatherEditor; +struct CSEditor; struct ExponentialHeightFog; struct HDRDisplay; struct ScreenshotFeature; @@ -95,7 +95,7 @@ namespace globals extern HDRDisplay hdrDisplay; extern RenderDoc renderDoc; extern ScreenshotFeature screenshotFeature; - extern WeatherEditor weatherEditor; + extern CSEditor csEditor; extern ExponentialHeightFog exponentialHeightFog; extern TruePBR truePBR; extern Skin skin; diff --git a/src/Menu.cpp b/src/Menu.cpp index cb6b9c253e..cde6f6fc5d 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -44,8 +44,8 @@ #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/ScreenshotFeature.h" #include "Features/VR.h" -#include "Features/WeatherEditor.h" -#include "WeatherEditor/EditorWindow.h" +#include "Features/CSEditor.h" +#include "CSEditor/EditorWindow.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -163,7 +163,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( OverlayToggleKey, ShaderBlockPrevKey, ShaderBlockNextKey, - WeatherEditorToggleKey, + CSEditorToggleKey, EnableShaderBlocking, FirstTimeSetupCompleted, SkipClearCacheConfirmation, @@ -342,7 +342,7 @@ void Menu::Load(json& o_json) migrateKey(o_json, "OverlayToggleKey", settings.OverlayToggleKey); migrateKey(o_json, "ShaderBlockPrevKey", settings.ShaderBlockPrevKey); migrateKey(o_json, "ShaderBlockNextKey", settings.ShaderBlockNextKey); - migrateKey(o_json, "WeatherEditorToggleKey", settings.WeatherEditorToggleKey); + migrateKey(o_json, "CSEditorToggleKey", settings.CSEditorToggleKey); migrateKey(o_json, "ScreenshotKey", settings.ScreenshotKey); // Helper for new smart serialization with error handling @@ -363,7 +363,7 @@ void Menu::Load(json& o_json) loadComboList(o_json, "OverlayToggleKey", settings.OverlayToggleKey); loadComboList(o_json, "ShaderBlockPrevKey", settings.ShaderBlockPrevKey); loadComboList(o_json, "ShaderBlockNextKey", settings.ShaderBlockNextKey); - loadComboList(o_json, "WeatherEditorToggleKey", settings.WeatherEditorToggleKey); + loadComboList(o_json, "CSEditorToggleKey", settings.CSEditorToggleKey); loadComboList(o_json, "ScreenshotKey", settings.ScreenshotKey); // Legacy support: If old config has Theme data and no SelectedThemePreset, load it @@ -428,7 +428,7 @@ void Menu::Save(json& o_json) InputCombo::ComboList::to_json(o_json["OverlayToggleKey"], settings.OverlayToggleKey); InputCombo::ComboList::to_json(o_json["ShaderBlockPrevKey"], settings.ShaderBlockPrevKey); InputCombo::ComboList::to_json(o_json["ShaderBlockNextKey"], settings.ShaderBlockNextKey); - InputCombo::ComboList::to_json(o_json["WeatherEditorToggleKey"], settings.WeatherEditorToggleKey); + InputCombo::ComboList::to_json(o_json["CSEditorToggleKey"], settings.CSEditorToggleKey); InputCombo::ComboList::to_json(o_json["ScreenshotKey"], settings.ScreenshotKey); } @@ -759,7 +759,7 @@ void Menu::DrawGeneralSettings() .settingOverlayToggleKey = settingOverlayToggleKey, .settingShaderBlockPrevKey = settingShaderBlockPrevKey, .settingShaderBlockNextKey = settingShaderBlockNextKey, - .settingWeatherEditorToggleKey = settingWeatherEditorToggleKey, + .settingCSEditorToggleKey = settingCSEditorToggleKey, .settingScreenshotKey = settingScreenshotKey }; @@ -885,7 +885,7 @@ void Menu::DrawOverlay() * @note This method contains Menu-specific logic and state management that makes it * inappropriate for extraction to a utility class. */ -static std::vector DeriveWeatherEditorKey(const std::vector& menuKey) +static std::vector DeriveCSEditorKey(const std::vector& menuKey) { bool hasShift = false; uint32_t baseKey = 0; @@ -981,14 +981,14 @@ void Menu::ProcessInputEventQueue() settings.ToggleKey = keys; settingToggleKey = false; if (!settings.FirstTimeSetupCompleted) - settings.WeatherEditorToggleKey = DeriveWeatherEditorKey(keys); + settings.CSEditorToggleKey = DeriveCSEditorKey(keys); } }, { &settings.SkipCompilationKey, &settingSkipCompilationKey, [this](std::vector keys) { settings.SkipCompilationKey = keys; settingSkipCompilationKey = false; } }, { &settings.EffectToggleKey, &settingsEffectsToggle, [this](std::vector keys) { settings.EffectToggleKey = keys; settingsEffectsToggle = false; } }, { &settings.OverlayToggleKey, &settingOverlayToggleKey, [this](std::vector keys) { settings.OverlayToggleKey = keys; settingOverlayToggleKey = false; } }, { &settings.ShaderBlockPrevKey, &settingShaderBlockPrevKey, [this](std::vector keys) { settings.ShaderBlockPrevKey = keys; settingShaderBlockPrevKey = false; } }, { &settings.ShaderBlockNextKey, &settingShaderBlockNextKey, [this](std::vector keys) { settings.ShaderBlockNextKey = keys; settingShaderBlockNextKey = false; } }, - { &settings.WeatherEditorToggleKey, &settingWeatherEditorToggleKey, [this](std::vector keys) { settings.WeatherEditorToggleKey = keys; settingWeatherEditorToggleKey = false; } }, + { &settings.CSEditorToggleKey, &settingCSEditorToggleKey, [this](std::vector keys) { settings.CSEditorToggleKey = keys; settingCSEditorToggleKey = false; } }, { &settings.ScreenshotKey, &settingScreenshotKey, [this](std::vector keys) { settings.ScreenshotKey = keys; settingScreenshotKey = false; } }, }; bool handled = false; @@ -1054,7 +1054,7 @@ void Menu::ProcessInputEventQueue() { settings.ShaderBlockPrevKey, [this, shaderCache]() { if (settings.EnableShaderBlocking) shaderCache->IterateShaderBlock(); } }, { settings.ShaderBlockNextKey, [this, shaderCache]() { if (settings.EnableShaderBlocking) shaderCache->IterateShaderBlock(false); } }, { settings.OverlayToggleKey, []() { Menu::GetSingleton()->overlayVisible = !Menu::GetSingleton()->overlayVisible; } }, - { settings.WeatherEditorToggleKey, []() { + { settings.CSEditorToggleKey, []() { auto* ew = EditorWindow::GetSingleton(); if (!ew) return; @@ -1065,7 +1065,7 @@ void Menu::ProcessInputEventQueue() // Locked or PlayMode → fully exit preview ew->ExitPreviewMode(); } else { - WeatherEditor::ToggleEditorWindow(); + CSEditor::ToggleEditorWindow(); } } }, { settings.ScreenshotKey, []() { @@ -1101,7 +1101,7 @@ void Menu::ProcessInputEventQueue() const std::vector* hotkeys[] = { &settings.ToggleKey, &settings.EffectToggleKey, &settings.OverlayToggleKey, &settings.ShaderBlockPrevKey, &settings.ShaderBlockNextKey, - &settings.WeatherEditorToggleKey, + &settings.CSEditorToggleKey, &settings.ScreenshotKey }; bool isHotkey = ShouldSwallowInput() && std::any_of(std::begin(hotkeys), std::end(hotkeys), @@ -1132,7 +1132,7 @@ void Menu::ProcessInputEventQueue() bool Menu::IsCapturingHotkeyInput() const { return settingToggleKey || settingSkipCompilationKey || settingsEffectsToggle || - settingOverlayToggleKey || settingShaderBlockPrevKey || settingShaderBlockNextKey || settingWeatherEditorToggleKey || settingScreenshotKey; + settingOverlayToggleKey || settingShaderBlockPrevKey || settingShaderBlockNextKey || settingCSEditorToggleKey || settingScreenshotKey; } void Menu::addToEventQueue(KeyEvent e) @@ -1197,22 +1197,22 @@ void Menu::SelectFeatureMenu(const std::string& featureName) /** * @brief Renders the standalone weather details window when enabled * - * Delegates to the WeatherEditor feature for rendering the weather details window + * Delegates to the CSEditor feature for rendering the weather details window * that can remain open even when the main menu is closed. This provides a simple - * coordination layer between the Menu system and the WeatherEditor feature. + * coordination layer between the Menu system and the CSEditor feature. */ void Menu::DrawWeatherDetailsWindow() { - if (!globals::features::weatherEditor.WeatherDetailsWindow.Enabled) { + if (!globals::features::csEditor.WeatherDetailsWindow.Enabled) { return; } - if (!globals::features::weatherEditor.loaded) { + if (!globals::features::csEditor.loaded) { return; } // Use Weather core feature for all window management and rendering - auto& weather = globals::features::weatherEditor; - bool* p_open = &globals::features::weatherEditor.WeatherDetailsWindow.Enabled; + auto& weather = globals::features::csEditor; + bool* p_open = &globals::features::csEditor.WeatherDetailsWindow.Enabled; weather.RenderWeatherDetailsWindow(p_open); } diff --git a/src/Menu.h b/src/Menu.h index 2e2b05774d..2add98b636 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -140,7 +140,7 @@ class Menu bool settingOverlayToggleKey = false; bool settingShaderBlockPrevKey = false; // Debug: capture shader block prev key bool settingShaderBlockNextKey = false; // Debug: capture shader block next key - bool settingWeatherEditorToggleKey = false; // Weather Editor toggle key + bool settingCSEditorToggleKey = false; // CS Editor toggle key bool settingScreenshotKey = false; // Screenshot capture key // Font caching (made public for ThemeManager and OverlayRenderer access) @@ -203,11 +203,11 @@ class Menu UIIcon logo; // New logo icon UIIcon search; // Search icon for search bars UIIcon featureSettingRevert; // Feature revert settings icon - UIIcon applyToGame; // Apply changes to game icon (weather editor) - UIIcon pauseTime; // Pause time icon (weather editor) - UIIcon undo; // Undo icon (weather editor) - UIIcon freeCamera; // Free camera preview icon (weather editor) - UIIcon playMode; // Play mode preview icon (weather editor) + UIIcon applyToGame; // Apply changes to game icon (CS editor) + UIIcon pauseTime; // Pause time icon (CS editor) + UIIcon undo; // Undo icon (CS editor) + UIIcon freeCamera; // Free camera preview icon (CS editor) + UIIcon playMode; // Play mode preview icon (CS editor) // Social media/external link icons UIIcon discord; @@ -405,7 +405,7 @@ class Menu std::vector OverlayToggleKey = { InputCombo::Keyboard(VK_F10) }; // Global overlay toggle key for all overlays std::vector ShaderBlockPrevKey = { InputCombo::Keyboard(VK_PRIOR) }; // Debug: cycle backward through shaders (PageUp) std::vector ShaderBlockNextKey = { InputCombo::Keyboard(VK_NEXT) }; // Debug: cycle forward through shaders (PageDown) - std::vector WeatherEditorToggleKey = { InputCombo::Keyboard(VK_SHIFT), InputCombo::Keyboard(VK_END) }; // Weather Editor toggle key + std::vector CSEditorToggleKey = { InputCombo::Keyboard(VK_SHIFT), InputCombo::Keyboard(VK_END) }; // CS Editor toggle key std::vector ScreenshotKey = { InputCombo::Keyboard(VK_SNAPSHOT) }; // Screenshot capture key bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed @@ -507,4 +507,4 @@ class Menu void ProcessInputEventQueue(); bool IsCapturingHotkeyInput() const; winrt::com_ptr dxgiAdapter3; -}; \ No newline at end of file +}; diff --git a/src/Menu/BackgroundBlur.cpp b/src/Menu/BackgroundBlur.cpp index 6f6f41426f..625840782a 100644 --- a/src/Menu/BackgroundBlur.cpp +++ b/src/Menu/BackgroundBlur.cpp @@ -47,7 +47,7 @@ namespace BackgroundBlur { std::mutex resourceMutex; bool enabled = false; - bool weatherEditorActive = false; + bool csEditorActive = false; // DirectX resources (RAII managed) winrt::com_ptr vertexShader; @@ -475,7 +475,7 @@ namespace BackgroundBlur windowConstants.windowParams[0] = cornerRadius; windowConstants.windowParams[1] = static_cast(sourceDesc.Width); windowConstants.windowParams[2] = static_cast(sourceDesc.Height); - windowConstants.windowParams[3] = weatherEditorActive ? 1.0f : 0.0f; + windowConstants.windowParams[3] = csEditorActive ? 1.0f : 0.0f; context->UpdateSubresource(windowConstantBuffer.get(), 0, nullptr, &windowConstants, 0, 0); auto windowConstantBufferPtr = windowConstantBuffer.get(); context->PSSetConstantBuffers(1, 1, &windowConstantBufferPtr); @@ -548,14 +548,14 @@ namespace BackgroundBlur enabled = enable; } - void SetWeatherEditorActive(bool active) + void SetCSEditorActive(bool active) { - weatherEditorActive = active; + csEditorActive = active; } - bool IsWeatherEditorActive() + bool IsCSEditorActive() { - return weatherEditorActive; + return csEditorActive; } void RenderBackgroundBlur() @@ -654,8 +654,8 @@ namespace BackgroundBlur CreateBlurTextures(texDesc.Width, texDesc.Height, texDesc.Format); } - // Weather editor mode: single fullscreen blur pass (better perf than per-window) - if (weatherEditorActive) { + // CS editor mode: single fullscreen blur pass (better perf than per-window) + if (csEditorActive) { ImVec2 screenMin = { 0, 0 }; ImVec2 screenMax = { static_cast(texDesc.Width), static_cast(texDesc.Height) }; PerformBlur(currentTexture.get(), sourceSRV, currentRTV.get(), screenMin, screenMax, 0.0f, uiBuffer.srv, uiBuffer.rtv); diff --git a/src/Menu/BackgroundBlur.h b/src/Menu/BackgroundBlur.h index 0919fe7a90..955f97b620 100644 --- a/src/Menu/BackgroundBlur.h +++ b/src/Menu/BackgroundBlur.h @@ -27,8 +27,8 @@ namespace BackgroundBlur void SetEnabled(bool enable); - /// When true, a single fullscreen blur replaces per-window blur (weather editor mode) - void SetWeatherEditorActive(bool active); - bool IsWeatherEditorActive(); + /// When true, a single fullscreen blur replaces per-window blur (CS editor mode) + void SetCSEditorActive(bool active); + bool IsCSEditorActive(); } // namespace BackgroundBlur diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index df8dde47ec..7cf6a55ceb 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -523,15 +523,15 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGui::TextDisabled("%s", pressKeyText); } - // Weather Editor hotkey status — updates live as user picks keys + // CS Editor hotkey status — updates live as user picks keys { - auto& weatherKey = menu->GetSettings().WeatherEditorToggleKey; + auto& weatherKey = menu->GetSettings().CSEditorToggleKey; if (weatherKey.empty()) { - const char* warnText = "Weather Editor hotkey unbound \xe2\x80\x94 chosen key uses Shift"; + const char* warnText = "CS Editor hotkey unbound \xe2\x80\x94 chosen key uses Shift"; centerText(warnText); ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.0f, 1.0f), "%s", warnText); } else { - std::string infoStr = "Weather Editor hotkey will be: " + Util::Input::KeyIdToString(weatherKey); + std::string infoStr = "CS Editor hotkey will be: " + Util::Input::KeyIdToString(weatherKey); centerText(infoStr.c_str()); ImGui::TextDisabled("%s", infoStr.c_str()); } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 01d5f619e9..d399339e05 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -18,7 +18,7 @@ #include "ShaderCache.h" #include "State.h" #include "Util.h" -#include "WeatherEditor/EditorWindow.h" +#include "CSEditor/EditorWindow.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 4caf0a1aa0..a0322dbeb7 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -372,10 +372,10 @@ void SettingsTabRenderer::RenderKeybindingsTab( "Change##OverlayToggle"); Util::InputComboWidget( - "Weather Editor Toggle Key:", - settings.WeatherEditorToggleKey, - state.settingWeatherEditorToggleKey, - "Change##WeatherEditorToggle"); + "CS Editor Toggle Key:", + settings.CSEditorToggleKey, + state.settingCSEditorToggleKey, + "Change##CSEditorToggle"); Util::InputComboWidget( "Screenshot Key:", @@ -1226,4 +1226,4 @@ void SettingsTabRenderer::RenderColorsTab() ImGui::EndTabItem(); } -} \ No newline at end of file +} diff --git a/src/Menu/SettingsTabRenderer.h b/src/Menu/SettingsTabRenderer.h index 7d6b1542b0..b7cea06884 100644 --- a/src/Menu/SettingsTabRenderer.h +++ b/src/Menu/SettingsTabRenderer.h @@ -19,7 +19,7 @@ class SettingsTabRenderer bool& settingOverlayToggleKey; bool& settingShaderBlockPrevKey; // Debug: shader block previous key bool& settingShaderBlockNextKey; // Debug: shader block next key - bool& settingWeatherEditorToggleKey; // Weather Editor toggle key + bool& settingCSEditorToggleKey; // CS Editor toggle key bool& settingScreenshotKey; // Screenshot capture key }; @@ -38,4 +38,4 @@ class SettingsTabRenderer static void RenderFontsTab(); static void RenderStylingTab(); static void RenderColorsTab(); -}; \ No newline at end of file +}; diff --git a/src/State.cpp b/src/State.cpp index 253efca58e..ad8f6cd098 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -16,7 +16,7 @@ #include "Features/Upscaling.h" #include "Features/VRStereoOptimizations.h" #include "Features/VolumetricShadows.h" -#include "Features/WeatherEditor.h" +#include "Features/CSEditor.h" #include "Menu.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" @@ -53,7 +53,7 @@ void State::Draw() auto& terrainBlending = globals::features::terrainBlending; auto& terrainHelper = globals::features::terrainHelper; auto& cloudShadows = globals::features::cloudShadows; - auto& weatherEditor = globals::features::weatherEditor; + auto& csEditor = globals::features::csEditor; auto& skin = globals::features::skin; auto& truePBR = globals::features::truePBR; auto context = globals::d3d::context; @@ -63,7 +63,7 @@ void State::Draw() // Process deferred cell transitions (interior detection) SceneSettingsManager::GetSingleton()->Update(); - if (weatherEditor.loaded) { + if (csEditor.loaded) { ZoneScopedN("WeatherManager::UpdateFeatures"); WeatherManager::GetSingleton()->UpdateFeatures(); } diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 404d7cfdb8..045b55147c 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,6 +1,6 @@ #include "UI.h" -#include "../WeatherEditor/EditorWindow.h" +#include "../CSEditor/EditorWindow.h" #include "D3D.h" #include "FileSystem.h" #include "Menu.h" @@ -2108,7 +2108,7 @@ namespace Util ImGui::TextWrapped("This setting is controlled by the current weather (%s).", currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open CS Editor"); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } @@ -2162,7 +2162,7 @@ namespace Util ImGui::TextWrapped("This setting is controlled by the current weather (%s).", currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open CS Editor"); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } @@ -2213,7 +2213,7 @@ namespace Util ImGui::TextWrapped("This setting is controlled by the current weather (%s).", currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open CS Editor"); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } @@ -2264,7 +2264,7 @@ namespace Util ImGui::TextWrapped("This setting is controlled by the current weather (%s).", currentWeathers.currentWeather ? currentWeathers.currentWeather->GetFormEditorID() : "Unknown"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open Weather Editor"); + ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f), "Click to open CS Editor"); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } From 055d508a85b322e3f67dc3fab6c1f80e1ac072a9 Mon Sep 17 00:00:00 2001 From: FIocker Date: Mon, 1 Jun 2026 18:27:20 +0200 Subject: [PATCH 09/55] fix(emat): allow solid-black height-only masks (#2441) --- package/Shaders/Lighting.hlsl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 3a7455c8dd..d89703119f 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -1120,12 +1120,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::extendedMaterialSettings.EnableComplexMaterial) { const float kMaskEpsilon = (4.0 / 255.0); - complexMaterial = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, 15).w < (1.0 - kMaskEpsilon); - - // Detect texture saved in the wrong format - if ((abs(envMaskSample.x - envMaskSample.y) < kMaskEpsilon) && - (abs(envMaskSample.x - envMaskSample.z) < kMaskEpsilon) && - (abs(envMaskSample.y - envMaskSample.z) < kMaskEpsilon)) + const float4 mipSample = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, 15); + complexMaterial = mipSample.w < (1.0 - kMaskEpsilon); + + const bool grayscaleMask = (abs(mipSample.x - mipSample.y) < kMaskEpsilon) && + (abs(mipSample.x - mipSample.z) < kMaskEpsilon) && + (abs(mipSample.y - mipSample.z) < kMaskEpsilon); + // Preserve height-only masks while rejecting grayscale environment masks + const bool solidBlackHeightMask = all(mipSample.xyz < kMaskEpsilon) && + mipSample.w > kMaskEpsilon && + mipSample.w < (1.0 - kMaskEpsilon); + if (grayscaleMask && !solidBlackHeightMask) complexMaterial = false; if (complexMaterial) { From 22ea0567d146873a7251e3a5cff4335a5c7c17de Mon Sep 17 00:00:00 2001 From: FIocker Date: Mon, 1 Jun 2026 19:22:09 +0200 Subject: [PATCH 10/55] fix(water): fix water blending (ghosting) and LOD gaps (#2440) --- package/Shaders/ISWaterBlend.hlsl | 10 ++++++++-- src/Features/UnifiedWater.cpp | 6 ++++-- src/Hooks.cpp | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/package/Shaders/ISWaterBlend.hlsl b/package/Shaders/ISWaterBlend.hlsl index 637da4ddcc..f3d9018d4a 100644 --- a/package/Shaders/ISWaterBlend.hlsl +++ b/package/Shaders/ISWaterBlend.hlsl @@ -1,5 +1,6 @@ #include "Common/DummyVSTexCoord.hlsl" #include "Common/FrameBuffer.hlsli" +#include "Common/Math.hlsli" #include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -75,11 +76,16 @@ PS_OUTPUT main(PS_INPUT input) 0.1, 0.95); historyFactor = NearFar_Menu_DistanceFactor.w * (distanceFactor * (waterMask * -0.85 + 0.95)); } + // Un-premultiply history so bilinear filtering against cleared pixels does not darken water edges + float3 historyColor = waterHistory.xyz / max(waterHistory.w, EPSILON_DIVISION); + historyFactor *= waterHistory.w; - finalColor = lerp(sourceColor, waterHistory.xyz, historyFactor); + finalColor = lerp(sourceColor, historyColor, historyFactor); } - psout.Color1 = float4(finalColor, WaterBlend::GetWaterCoverage(waterMask)); + float waterCoverage = WaterBlend::GetWaterCoverage(waterMask); + // Store premultiplied history so transparent clears filter without dark outlines + psout.Color1 = float4(finalColor * waterCoverage, waterCoverage); psout.Color = finalColor; return psout; diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 3c151449b6..05e2c5e224 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -481,8 +481,10 @@ void UnifiedWater::BGSTerrainNode_UpdateWaterMeshSubVisibility::thunk(const RE:: bool cull = false; if (x >= 0 && y >= 0 && x < length && y < length) { - if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any(RE::TESObjectCELL::CellState::kAttached, static_cast(6))) - cull = true; + if (const auto cell = gridCells->GetCell(x, y); cell && cell->cellState.any(RE::TESObjectCELL::CellState::kAttached, static_cast(6))) { + // Keep LOD visible when a loaded dry cell has no active water to replace it + cull = cell->cellFlags.any(RE::TESObjectCELL::Flag::kHasWater); + } } child->SetAppCulled(cull); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 6c9aa5d612..04272dd855 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -232,6 +232,28 @@ namespace GrassExtensions }; } +namespace WaterBlendHistory +{ + struct BSImagespaceShader_Render + { + static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) + { + GET_INSTANCE_MEMBER(renderTargets, globals::game::shadowState) + + // Clear stale coverage left by discarded non-water pixels + const float clearColor[4] = { 0.f, 0.f, 0.f, 0.f }; + const auto target = renderTargets[1]; + globals::d3d::context->ClearRenderTargetView( + globals::game::renderer->GetRuntimeData().renderTargets[target].RTV, + clearColor); + + func(imageSpaceShader, shape, param); + } + + static inline REL::Relocation func; + }; +} + struct IDXGISwapChain_Present { static HRESULT WINAPI thunk(IDXGISwapChain* This, UINT SyncInterval, UINT Flags) @@ -902,6 +924,7 @@ namespace Hooks logger::info("Hooking BSImagespaceShader"); stl::detour_thunk(REL::RelocationID(100952, 107734)); + stl::write_vfunc<0x1, WaterBlendHistory::BSImagespaceShader_Render>(RE::VTABLE_BSImagespaceShaderISWaterBlend[3]); logger::info("Hooking BSComputeShader"); stl::write_vfunc<0x02, CSShadersSupport::BSComputeShader_Dispatch>(RE::VTABLE_BSComputeShader[0]); From 26c10c40d435e2625fe08dcd4889f4b64ff5fae0 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:16:22 +0200 Subject: [PATCH 11/55] ci: fix perm for hotfix workflow (#2451) --- .github/workflows/release-hotfix.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release-hotfix.yaml b/.github/workflows/release-hotfix.yaml index 3add2299f2..43dc5c9eb3 100644 --- a/.github/workflows/release-hotfix.yaml +++ b/.github/workflows/release-hotfix.yaml @@ -286,10 +286,7 @@ jobs: - name: Open PR against hotfix branch if: ${{ !inputs.dry_run && steps.cherry.outputs.picked_count != '0' }} env: - # GITHUB_TOKEN suffices here: the workflow declares - # `permissions: pull-requests: write`, and creating the - # candidate PR requires no branch-protection bypass. - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} # Pass via env (not ${{ }} interpolation) so the literal # backticks inside SOURCE_DESC don't get re-evaluated by # bash as command substitution. From d49f15046659e32434e4117fdc451e1816be5cb3 Mon Sep 17 00:00:00 2001 From: davo0411 Date: Tue, 2 Jun 2026 19:30:16 +1000 Subject: [PATCH 12/55] perf: remove per-update heap allocation from feature cbuffer path (#2450) --- src/FeatureBuffer.cpp | 13 +++++++++---- src/FeatureBuffer.h | 2 ++ src/State.cpp | 4 +--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index f68f563e57..eaef6d6cdc 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -1,5 +1,7 @@ #include "FeatureBuffer.h" +#include + #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" #include "Features/ExponentialHeightFog.h" @@ -22,17 +24,20 @@ template std::pair _GetFeatureBufferData(Ts... feat_datas) { - size_t totalSize = (... + sizeof(Ts)); - auto data = new unsigned char[totalSize]; + // The packed size is a compile-time constant, so reuse one aligned, thread-local buffer + // instead of allocating/freeing every UpdateSharedData call. The returned pointer is + // non-owning and must NOT be deleted by the caller. + constexpr size_t totalSize = (... + sizeof(Ts)); + alignas(16) static thread_local std::array storage; size_t offset = 0; ([&] { - *((decltype(feat_datas)*)(data + offset)) = feat_datas; + *reinterpret_cast(storage.data() + offset) = feat_datas; offset += sizeof(decltype(feat_datas)); }(), ...); - return std::make_pair(data, totalSize); + return std::make_pair(storage.data(), storage.size()); } std::pair GetFeatureBufferData(bool a_inWorld) diff --git a/src/FeatureBuffer.h b/src/FeatureBuffer.h index c3145c7421..ab0e13f609 100644 --- a/src/FeatureBuffer.h +++ b/src/FeatureBuffer.h @@ -1,3 +1,5 @@ #pragma once +// Returns a pointer into reused, thread-local storage and the packed size. +// The pointer is non-owning: do NOT delete[] it. std::pair GetFeatureBufferData(bool a_early); \ No newline at end of file diff --git a/src/State.cpp b/src/State.cpp index ad8f6cd098..1e4bca201c 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -747,8 +747,8 @@ void State::SetupResources() sharedDataCB = new ConstantBuffer(ConstantBufferDesc()); auto [data, size] = GetFeatureBufferData(false); + (void)data; featureDataCB = new ConstantBuffer(ConstantBufferDesc((uint32_t)size)); - delete[] data; // Grab main texture to get resolution // VR cannot use viewport->screenWidth/Height as it's the desktop preview window's resolution and not HMD @@ -1032,8 +1032,6 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b auto [data, size] = GetFeatureBufferData(a_inWorld); featureDataCB->Update(data, size); - - delete[] data; } auto* srv = Util::GetCurrentSceneDepthSRV(true); From a9681df03946a8231eb6fce69c3bbf239e788fc2 Mon Sep 17 00:00:00 2001 From: jiayev Date: Tue, 2 Jun 2026 17:36:24 +0800 Subject: [PATCH 13/55] feat: localization (#2416) --- .claude/CLAUDE.md | 63 + .github/workflows/pr-i18n.yaml | 90 + TRANSLATING.md | 119 + .../CommunityShaders/Translations/en.json | 2094 +++++++++++++++++ .../CommunityShaders/Translations/zh_CN.json | 2093 ++++++++++++++++ src/CSEditor/EditorWindow.cpp | 372 +-- src/CSEditor/InteriorOnlyPanel.cpp | 44 +- src/CSEditor/LightEditor.cpp | 83 +- src/CSEditor/PaletteWindow.cpp | 81 +- src/CSEditor/Weather/CellLightingWidget.cpp | 80 +- .../Weather/LightingTemplateWidget.cpp | 35 +- src/CSEditor/Weather/PrecipitationWidget.cpp | 73 +- .../Weather/ReferenceEffectWidget.cpp | 29 +- src/CSEditor/Weather/SimpleFormWidget.h | 9 +- .../Weather/VolumetricLightingWidget.cpp | 43 +- src/CSEditor/Weather/WeatherWidget.cpp | 378 +-- src/CSEditor/WeatherUtils.cpp | 251 +- src/CSEditor/WeatherUtils.h | 38 +- src/CSEditor/Widget.cpp | 69 +- src/Feature.cpp | 32 +- src/Feature.h | 3 + src/FeatureIssues.cpp | 304 ++- src/Features/CSEditor.cpp | 236 +- src/Features/CSEditor.h | 22 +- src/Features/CloudShadows.cpp | 11 +- src/Features/CloudShadows.h | 18 +- src/Features/DynamicCubemaps.cpp | 30 +- src/Features/DynamicCubemaps.h | 18 +- src/Features/ExponentialHeightFog.cpp | 46 +- src/Features/ExponentialHeightFog.h | 15 +- src/Features/ExtendedMaterials.cpp | 57 +- src/Features/ExtendedMaterials.h | 18 +- src/Features/ExtendedTranslucency.cpp | 58 +- src/Features/ExtendedTranslucency.h | 11 +- src/Features/GrassCollision.cpp | 8 +- src/Features/GrassCollision.h | 17 +- src/Features/GrassLighting.cpp | 62 +- src/Features/GrassLighting.h | 18 +- src/Features/HDRDisplay.cpp | 91 +- src/Features/HDRDisplay.h | 15 +- src/Features/HairSpecular.cpp | 73 +- src/Features/HairSpecular.h | 16 +- src/Features/IBL.cpp | 70 +- src/Features/IBL.h | 17 +- src/Features/InteriorSun.cpp | 22 +- src/Features/InteriorSun.h | 16 +- src/Features/InverseSquareLighting.h | 15 +- src/Features/LODBlending.cpp | 27 +- src/Features/LODBlending.h | 18 +- src/Features/LightLimitFix.cpp | 30 +- src/Features/LightLimitFix.h | 18 +- src/Features/LinearLighting.cpp | 75 +- src/Features/LinearLighting.h | 13 +- src/Features/PerformanceOverlay.cpp | 81 +- src/Features/PerformanceOverlay.h | 13 +- src/Features/RenderDoc.cpp | 75 +- src/Features/RenderDoc.h | 6 +- src/Features/ScreenSpaceGI.cpp | 138 +- src/Features/ScreenSpaceGI.h | 25 +- src/Features/ScreenSpaceShadows.cpp | 36 +- src/Features/ScreenSpaceShadows.h | 16 +- src/Features/ScreenshotFeature.cpp | 58 +- src/Features/ScreenshotFeature.h | 1 + src/Features/Skin.cpp | 115 +- src/Features/Skin.h | 15 +- src/Features/SkySync.cpp | 61 +- src/Features/SkySync.h | 19 +- src/Features/Skylighting.cpp | 21 +- src/Features/Skylighting.h | 18 +- src/Features/SubsurfaceScattering.cpp | 67 +- src/Features/SubsurfaceScattering.h | 18 +- src/Features/TerrainBlending.cpp | 8 +- src/Features/TerrainBlending.h | 18 +- src/Features/TerrainHelper.h | 17 +- src/Features/TerrainShadows.cpp | 10 +- src/Features/TerrainShadows.h | 18 +- src/Features/TerrainVariation.cpp | 21 +- src/Features/TerrainVariation.h | 16 +- src/Features/UnifiedWater.cpp | 24 +- src/Features/UnifiedWater.h | 16 +- src/Features/Upscaling.cpp | 138 +- src/Features/Upscaling.h | 15 +- src/Features/VR.h | 19 +- src/Features/VRStereoOptimizations.cpp | 38 +- src/Features/VolumetricLighting.cpp | 27 +- src/Features/VolumetricLighting.h | 18 +- src/Features/VolumetricShadows.h | 16 +- src/Features/WaterEffects.h | 18 +- src/Features/WetnessEffects.cpp | 313 ++- src/Features/WetnessEffects.h | 17 +- src/I18n/I18n.cpp | 413 ++++ src/I18n/I18n.h | 208 ++ src/Menu.cpp | 34 +- src/Menu/AdvancedSettingsRenderer.cpp | 304 +-- src/Menu/FeatureListRenderer.cpp | 174 +- src/Menu/Fonts.cpp | 3 + src/Menu/HomePageRenderer.cpp | 162 +- src/Menu/MenuHeaderRenderer.cpp | 56 +- src/Menu/OverlayRenderer.cpp | 9 +- src/Menu/SettingsTabRenderer.cpp | 564 +++-- src/Menu/ThemeManager.cpp | 219 ++ src/State.cpp | 20 +- src/TruePBR.cpp | 84 +- src/TruePBR.h | 1 + src/Utils/FileSystem.cpp | 5 + src/Utils/FileSystem.h | 6 + src/Utils/GameSetting.cpp | 3 +- src/Utils/Subrect.cpp | 1 + src/Utils/UI.cpp | 56 +- src/Utils/UI.h | 2 +- src/XSEPlugin.cpp | 5 + tools/extract-i18n.py | 418 ++++ 112 files changed, 9300 insertions(+), 2541 deletions(-) create mode 100644 .github/workflows/pr-i18n.yaml create mode 100644 TRANSLATING.md create mode 100644 package/SKSE/Plugins/CommunityShaders/Translations/en.json create mode 100644 package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json create mode 100644 src/I18n/I18n.cpp create mode 100644 src/I18n/I18n.h create mode 100644 tools/extract-i18n.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e6b4da4993..cb0ac9c674 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -561,3 +561,66 @@ Full details: [Developers wiki — Patch Release Process](https://github.com/com - **Graphics State Corruption**: Minimize DirectX state changes; restore state after modifications - **Thread Safety**: Graphics operations must consider Skyrim's rendering thread vs game logic thread - **DRY Violations in Cross-Cutting Refactors**: When adding a utility pattern across many files (e.g., resource naming, debug hooks), check whether the implementation exists in multiple places before writing a new one. For example, `Buffer.h` helper classes and raw `device->Create*` callsites both need `SetResourceName` — ensure they share a single implementation, not duplicate GUID definitions or parallel helper functions. Use a forward declaration in headers to delegate to the canonical implementation in `Utils/D3D.cpp` rather than re-implementing inline. + +## Internationalization (i18n) System + +### Using Translations in Code + +All user-visible strings must use the translation system. The source of truth for English strings is `package/SKSE/Plugins/CommunityShaders/Translations/en.json`. + +**API**: + +```cpp +// T() macro: key + inline English default +ImGui::Text("%s", T("menu.faq.q10", "My new FAQ question?")); + +// TKEY macro: for feature files, prefixes are defined to keep keys short +#define I18N_KEY_PREFIX "feature.my_feature." +ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), &settings.enabled); +#undef I18N_KEY_PREFIX +``` + +**After adding new translatable strings**, regenerate `en.json`: + +```bash +python tools/extract-i18n.py --write +``` + +### Key Naming Convention + +``` +menu.. — Menu UI labels +menu.._tooltip — Tooltip text +feature.. — Feature settings +overlay. — Overlay messages +common. — Shared/reused text +ui. — Utility UI +weather_editor. — Weather editor +``` + +### Translation Rules (Must Follow When Writing Strings) + +| Rule | Detail | +| --------------------------------- | ------------------------------------------------------------- | +| **Translate values, not keys** | Keys (left side of JSON) are never translated | +| **Preserve placeholders** | `{version}`, `{count}`, `{key}` must be kept in all languages | +| **Preserve format specifiers** | `%s`, `%d`, `%.1f` must be kept | +| **`\n` = line break** | Line break positions may be adjusted | +| **Don't translate `##` suffixes** | If a value contains `##xxx`, the part after `##` stays as-is | +| **Partial translations OK** | Missing keys automatically fall back to English | + +### CI Validation (`pr-i18n.yaml`) + +The CI workflow checks: + +- `en.json` is in sync with source code (`--check`) +- No orphaned keys exist (`--orphans`) +- Translation files have valid JSON format +- Placeholders `{name}` are consistent across languages + +**Before submitting PRs that add/modify UI strings**, run locally: + +```bash +python tools/extract-i18n.py --check +python tools/extract-i18n.py --orphans +``` diff --git a/.github/workflows/pr-i18n.yaml b/.github/workflows/pr-i18n.yaml new file mode 100644 index 0000000000..0d19bec3d8 --- /dev/null +++ b/.github/workflows/pr-i18n.yaml @@ -0,0 +1,90 @@ +name: "PR: i18n Check" + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "src/**" + - "package/SKSE/Plugins/CommunityShaders/Translations/**" + - "tools/extract-i18n.py" + +permissions: + contents: read + +jobs: + i18n-check: + name: Verify en.json is in sync with source + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Check en.json is up to date + run: python tools/extract-i18n.py --check + + - name: Check for orphaned keys + run: python tools/extract-i18n.py --orphans + + - name: Validate translation file formats + run: | + python -c " + import json, sys, pathlib + + translations_dir = pathlib.Path('package/SKSE/Plugins/CommunityShaders/Translations') + if not translations_dir.exists(): + print('No Translations directory found') + sys.exit(0) + + # Load en.json keys as reference + en_path = translations_dir / 'en.json' + if not en_path.exists(): + print('ERROR: en.json not found') + sys.exit(1) + + with open(en_path, encoding='utf-8') as f: + en_data = json.load(f) + en_keys = {k for k in en_data if k != '_meta'} + + errors = [] + for path in sorted(translations_dir.glob('*.json')): + if path.name == 'en.json': + continue + try: + with open(path, encoding='utf-8') as f: + data = json.load(f) + except json.JSONDecodeError as e: + errors.append(f'{path.name}: Invalid JSON - {e}') + continue + + if not isinstance(data, dict): + errors.append(f'{path.name}: Root must be a JSON object') + continue + + # Check for keys not in en.json (stale/typo keys) + locale_keys = {k for k in data if k != '_meta'} + extra_keys = locale_keys - en_keys + if extra_keys: + errors.append(f'{path.name}: {len(extra_keys)} key(s) not in en.json: {sorted(extra_keys)[:5]}') + + # Check placeholder consistency + import re + placeholder_re = re.compile(r'\{(\w+)\}') + for key in locale_keys & en_keys: + en_placeholders = set(placeholder_re.findall(en_data[key])) + locale_placeholders = set(placeholder_re.findall(data[key])) + if en_placeholders != locale_placeholders: + errors.append(f'{path.name}: Key \"{key}\" has mismatched placeholders: expected {en_placeholders}, got {locale_placeholders}') + + if errors: + print(f'Found {len(errors)} error(s):') + for e in errors: + print(f' - {e}') + sys.exit(1) + else: + print(f'All translation files valid. Checked {len(list(translations_dir.glob(\"*.json\"))) - 1} locale(s).') + " diff --git a/TRANSLATING.md b/TRANSLATING.md new file mode 100644 index 0000000000..0780d4682a --- /dev/null +++ b/TRANSLATING.md @@ -0,0 +1,119 @@ +# Translating Community Shaders + +Community Shaders supports multiple languages through a JSON-based translation system. +This document explains how to contribute translations. + +## For Translators (No Coding Required) + +### Option A: Via Weblate (Recommended) + +The easiest way to contribute translations is through our hosted Weblate instance: + +1. Visit: **[hosted.weblate.org/projects/community-shaders](https://hosted.weblate.org/projects/community-shaders/)** _(link will be active once configured)_ +2. Create an account or log in with GitHub +3. Select your language +4. Translate strings in the web interface +5. Your translations are automatically submitted as PRs + +Weblate provides: + +- Translation memory and suggestions +- Consistency checks +- Placeholder validation (`{name}` must be preserved) +- Progress tracking per language + +### Option B: Direct PR on GitHub + +1. Fork the repository +2. Copy `package/SKSE/Plugins/CommunityShaders/Translations/en.json` to a new file named with your locale code (e.g., `zh_CN.json`, `ja.json`, `de.json`) +3. Translate the string values (NOT the keys) +4. Submit a Pull Request + +## Translation File Format + +```json +{ + "_meta": { + "language": "简体中文", + "locale": "zh_CN", + "version": "1.0.0", + "authors": ["Your Name"] + }, + "menu.home.welcome": "欢迎使用 Community Shaders {version}", + "menu.faq.q1": "什么是 Community Shaders?", + ... +} +``` + +### Rules + +| Rule | Example | +| --------------------------------- | ---------------------------------------------------- | +| **Translate values, not keys** | `"menu.faq.q1": "翻译这里"` — key 左边不改 | +| **Preserve placeholders** | `{version}`, `{count}`, `{key}` 必须保留,位置可调整 | +| **Preserve format specifiers** | `%s`, `%d`, `%.1f` 必须保留 | +| **`\n` = line break** | 可以调整分行位置 | +| **`_meta.language`** | 用该语言自身书写(如 "日本語" 而非 "Japanese") | +| **Don't translate `##` suffixes** | 如果值中包含 `##xxx`,不翻译 `##` 后面的部分 | +| **Partial translations OK** | 缺失的 key 会自动 fallback 到英文 | + +### Locale Codes + +Use standard BCP 47-style codes: + +| Code | Language | +| ------- | ------------------ | +| `zh_CN` | 简体中文 | +| `zh_TW` | 繁體中文 | +| `ja` | 日本語 | +| `ko` | 한국어 | +| `de` | Deutsch | +| `fr` | Français | +| `es` | Español | +| `pt_BR` | Português (Brasil) | +| `ru` | Русский | +| `it` | Italiano | +| `pl` | Polski | + +## For Developers + +### Adding New Translatable Strings + +```cpp +// 1. Use T() with inline default in source code +ImGui::Text("%s", T("menu.faq.q10", "My new FAQ question?")); + +// 2. For Feature files, use TKEY macro for shorter keys +#define I18N_KEY_PREFIX "feature.my_feature." +ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), &settings.enabled); +#undef I18N_KEY_PREFIX + +// 3. Regenerate en.json +// Run: python tools/extract-i18n.py --write +``` + +### CI Validation + +The `pr-i18n.yaml` workflow checks: + +- `en.json` is in sync with source code (`--check`) +- No orphaned keys exist (`--orphans`) +- Translation files have valid JSON format +- Placeholders `{name}` are consistent across languages + +### Key Naming Convention + +``` +menu.. — Menu UI labels +menu.._tooltip — Tooltip text +feature.. — Feature settings +overlay. — Overlay messages +common. — Shared/reused text +ui. — Utility UI +weather_editor. — Weather editor +``` + +## CJK Font Support + +CJK languages (Chinese, Japanese, Korean) require fonts with appropriate glyph coverage. +Community Shaders uses system CJK fonts by default. diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json new file mode 100644 index 0000000000..b8dbab9f52 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -0,0 +1,2094 @@ +{ + "_meta": { + "language": "English", + "locale": "en", + "auto_generated": true, + "generator": "tools/extract-i18n.py", + "note": "DO NOT EDIT MANUALLY. Run: python tools/extract-i18n.py --write" + }, + "common.active": "Active", + "common.inactive": "Inactive", + "cs_editor.actions": "Actions", + "cs_editor.active": "Active:", + "cs_editor.active_click_pause": "Active - click to pause", + "cs_editor.add": "Add", + "cs_editor.add_new_marker": "Add new marker", + "cs_editor.ambient_color": "Ambient Color", + "cs_editor.ambient_directional": "Ambient & Directional", + "cs_editor.apply": "Apply", + "cs_editor.apply_changes": "Apply changes to the game", + "cs_editor.art_object": "Art Object", + "cs_editor.attach_to_camera": "Attach To Camera", + "cs_editor.auto_apply_changes": "Auto-Apply Changes", + "cs_editor.auto_apply_changes_tooltip": "Automatically apply weather changes to the game as you edit", + "cs_editor.box_size": "Box Size", + "cs_editor.cancel": "Cancel", + "cs_editor.categories": "Categories", + "cs_editor.category_cell_lighting": "Cell Lighting", + "cs_editor.category_imagespace": "ImageSpace", + "cs_editor.category_interior_only": "Interior Only", + "cs_editor.category_lens_flare": "Lens Flare", + "cs_editor.category_lighting_editor": "Light Editor", + "cs_editor.category_lighting_template": "Lighting Template", + "cs_editor.category_lightning": "Lightning", + "cs_editor.category_precipitation": "Precipitation", + "cs_editor.category_shader_particle": "Shader Particle Geometry", + "cs_editor.category_sun": "Sun", + "cs_editor.category_visual_effect": "Visual Effect", + "cs_editor.category_visual_effects": "Visual Effects", + "cs_editor.category_volumetric_lighting": "Volumetric Lighting", + "cs_editor.category_weather": "Weather", + "cs_editor.category_weather_transition": "Weather Transition", + "cs_editor.category_wind": "Wind", + "cs_editor.cell_lighting_interior_only": "Cell Lighting is only available for interior cells.", + "cs_editor.center_offset_max": "Center Offset Max", + "cs_editor.center_offset_min": "Center Offset Min", + "cs_editor.changes_require_manual_apply": "(Changes require manual apply)", + "cs_editor.clear_favorites": "Clear Favorites", + "cs_editor.clear_favourite": "Clear favourite", + "cs_editor.clear_recent_history": "Clear Recent History", + "cs_editor.click_plus_add": "Click + to add settings that will only apply in interiors.", + "cs_editor.click_to_copy": "Click to copy", + "cs_editor.clip_distance": "Clip Distance", + "cs_editor.close_all_widgets": "Close All {} Widgets", + "cs_editor.close_cs_editor": "Close CS Editor (Esc)", + "cs_editor.cloud_alpha": "Cloud Alpha", + "cs_editor.cloud_color": "Cloud Color", + "cs_editor.cloud_layer": "Cloud Layer {}", + "cs_editor.cloud_layer_speed_x": "Cloud Layer Speed X", + "cs_editor.cloud_layer_speed_y": "Cloud Layer Speed Y", + "cs_editor.color": "Color", + "cs_editor.color_ambient": "Ambient", + "cs_editor.color_cloud_lod_ambient": "Cloud LOD Ambient", + "cs_editor.color_cloud_lod_diffuse": "Cloud LOD Diffuse", + "cs_editor.color_effect_lighting": "Effect Lighting", + "cs_editor.color_fog_far": "Fog Far", + "cs_editor.color_fog_near": "Fog Near", + "cs_editor.color_horizon": "Horizon", + "cs_editor.color_moon_glare": "Moon Glare", + "cs_editor.color_sky_lower": "Sky Lower", + "cs_editor.color_sky_statics": "Sky Statics", + "cs_editor.color_sky_upper": "Sky Upper", + "cs_editor.color_stars": "Stars", + "cs_editor.color_sun": "Sun", + "cs_editor.color_sun_glare": "Sun Glare", + "cs_editor.color_sunlight": "Sunlight", + "cs_editor.color_water_multiplier": "Water Multiplier", + "cs_editor.colors_3_plus": "(Colors used 3+ times will appear here)", + "cs_editor.colour": "Colour", + "cs_editor.colours": "Colours", + "cs_editor.confirm_delete_saved_file": "Are you sure you want to delete the saved settings file?", + "cs_editor.contribution": "Contribution", + "cs_editor.copy_all_from_parent": "Copy all parameter values from parent weather", + "cs_editor.cs_editor": "CS Editor", + "cs_editor.currently_exterior_cell": "You are currently in an exterior cell.", + "cs_editor.custom_color": "Custom Color", + "cs_editor.custom_color_contribution": "Custom Color Contribution", + "cs_editor.custom_overrides_tooltip_0": "This weather has custom overrides for this feature.", + "cs_editor.custom_overrides_tooltip_1": "Click to disable overrides and use global settings instead.", + "cs_editor.custom_overrides_tooltip_2": "(Settings will be preserved but not applied)", + "cs_editor.dalc_directional_x_max": "Directional +X", + "cs_editor.dalc_directional_x_min": "Directional -X", + "cs_editor.dalc_directional_y_max": "Directional +Y", + "cs_editor.dalc_directional_y_min": "Directional -Y", + "cs_editor.dalc_directional_z_max": "Directional +Z", + "cs_editor.dalc_directional_z_min": "Directional -Z", + "cs_editor.dalc_fresnel_power": "Fresnel Power", + "cs_editor.dalc_header": "Directional Ambient Lighting (DALC)", + "cs_editor.dalc_specular": "Specular", + "cs_editor.day": "Day", + "cs_editor.day_far": "Day Far", + "cs_editor.day_max": "Day Max", + "cs_editor.day_near": "Day Near", + "cs_editor.day_power": "Day Power", + "cs_editor.delete": "Delete", + "cs_editor.delete_all": "Delete All", + "cs_editor.delete_json_file": "Delete JSON file", + "cs_editor.delete_overwrite_file": "Delete overwrite file from disk", + "cs_editor.delete_saved_file": "Delete Saved File", + "cs_editor.delete_saved_file_tooltip": "Delete saved file", + "cs_editor.density_contribution": "Density Contribution", + "cs_editor.density_settings": "Density Settings", + "cs_editor.density_size": "Density Size", + "cs_editor.direction_x_minus": "X- (Left)", + "cs_editor.direction_x_plus": "X+ (Right)", + "cs_editor.direction_y_minus": "Y- (Back)", + "cs_editor.direction_y_plus": "Y+ (Front)", + "cs_editor.direction_z_minus": "Z- (Down)", + "cs_editor.direction_z_plus": "Z+ (Up)", + "cs_editor.directional_color": "Directional Color", + "cs_editor.directional_colors": "Directional Colors", + "cs_editor.directional_fade": "Directional Fade", + "cs_editor.directional_settings": "Directional Settings", + "cs_editor.directional_xy": "Directional XY", + "cs_editor.directional_z": "Directional Z", + "cs_editor.drag_colours_here": "Drag colours here to save as favourites.", + "cs_editor.drag_to_favourites": "Drag a colour here to add to favourites", + "cs_editor.edit_current_cell_lighting": "Edit Current Cell Lighting", + "cs_editor.editor_flags": "Editor Flags", + "cs_editor.editor_id": "Editor ID", + "cs_editor.editor_id_label": "EditorID: %s", + "cs_editor.editor_ui_scale": "Editor UI Scale", + "cs_editor.editor_ui_scale_tooltip": "Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)", + "cs_editor.effect_shader": "Effect Shader", + "cs_editor.enable": "Enable", + "cs_editor.enable_inherit_feature": "Enable 'Inherit From Parent' feature", + "cs_editor.enable_inherit_feature_tooltip": "Show checkboxes to copy settings from parent weather (editor-only feature)", + "cs_editor.enable_inherit_from_parent": "Enable Inherit From Parent", + "cs_editor.enable_inherit_tooltip": "Show inherit from parent options in weather widgets", + "cs_editor.enable_weather_overrides_hint": "Enable weather-specific overrides above to customize settings for this weather.", + "cs_editor.enabled_badge": "[Enabled]", + "cs_editor.exit_free_camera": "Exit Free Camera", + "cs_editor.exit_play_mode": "Exit Play Mode", + "cs_editor.face_target": "Face Target", + "cs_editor.falling_speed": "Falling Speed", + "cs_editor.fav": "Fav", + "cs_editor.fav_most_colours": "Favourite/most commonly used colours here.", + "cs_editor.fav_most_values": "Favourite/most commonly used values here.", + "cs_editor.favorites": "Favorites", + "cs_editor.favorites_count": "Favorites: %d", + "cs_editor.favourites": "Favourites", + "cs_editor.feature_specific_settings": "Configure feature-specific settings that will be applied when this weather is active. These override the feature's global settings for this weather only.", + "cs_editor.features": "Features", + "cs_editor.file": "File", + "cs_editor.file_label": "File: %s", + "cs_editor.filter_all": "All", + "cs_editor.filter_editor_id": "Editor ID", + "cs_editor.filter_file": "File", + "cs_editor.filter_form_id": "Form ID", + "cs_editor.filter_help": "Filter the object list by the selected column.\nAll: searches Editor ID, Form ID, File, and Status.\nStatus: hides items with no status marker when the search box is non-empty.\nCtrl+F: Focus search\nEnter: Open selected", + "cs_editor.filter_hint": "Filter... (Ctrl+F)", + "cs_editor.filter_status": "Status", + "cs_editor.flagged": "Flagged", + "cs_editor.flags": "Flags", + "cs_editor.fog_clamp": "Fog Clamp", + "cs_editor.fog_color_far": "Fog Color Far", + "cs_editor.fog_color_near": "Fog Color Near", + "cs_editor.fog_far": "Far", + "cs_editor.fog_max": "Max", + "cs_editor.fog_near": "Near", + "cs_editor.fog_power": "Fog Power", + "cs_editor.fog_power_short": "Power", + "cs_editor.force_this_weather": "Force This Weather", + "cs_editor.force_weather": "Force Weather", + "cs_editor.form_id": "Form ID", + "cs_editor.form_id_label": "FormID: %08X", + "cs_editor.form_record_references": "Form record references used by this weather.", + "cs_editor.form_reference_note": "This form is referenced by weather records. To change which form is used, edit the Records tab in the Weather widget.", + "cs_editor.free_camera_scroll": "Free Camera (scroll to adjust speed)", + "cs_editor.game_time": "Game Time", + "cs_editor.game_time_tooltip": "Adjust the current game time", + "cs_editor.general": "General", + "cs_editor.general_settings": "General Settings", + "cs_editor.global_settings_tooltip_0": "This weather uses global feature settings.", + "cs_editor.global_settings_tooltip_1": "Click to enable weather-specific overrides.", + "cs_editor.gravity_velocity": "Gravity Velocity", + "cs_editor.help": "Help", + "cs_editor.imagespace_label": "ImageSpace:", + "cs_editor.imagespaces_count": "ImageSpaces: %d", + "cs_editor.inherit_all": "Inherit All", + "cs_editor.inherit_ambient_color": "Inherit Ambient Color", + "cs_editor.inherit_clip_distance": "Inherit Clip Distance", + "cs_editor.inherit_directional_color": "Inherit Directional Color", + "cs_editor.inherit_directional_fade": "Inherit Directional Fade", + "cs_editor.inherit_directional_rotation": "Inherit Directional Rotation", + "cs_editor.inherit_flags_desc": "These flags control which lighting properties are inherited from the cell's lighting template.", + "cs_editor.inherit_fog_color": "Inherit Fog Color", + "cs_editor.inherit_fog_far": "Inherit Fog Far", + "cs_editor.inherit_fog_max_clamp": "Inherit Fog Max (Clamp)", + "cs_editor.inherit_fog_near": "Inherit Fog Near", + "cs_editor.inherit_fog_power": "Inherit Fog Power", + "cs_editor.inherit_from_parent": "Inherit from parent", + "cs_editor.inherit_from_parent_weather": "Inherit from parent weather", + "cs_editor.inherit_light_fade_distances": "Inherit Light Fade Distances", + "cs_editor.inherit_rotation": "Inherit Rotation", + "cs_editor.inherited_from_lighting_template": "Inherited from lighting template", + "cs_editor.inherited_from_parent_weather": "Inherited from parent weather", + "cs_editor.inheriting_from_parent": "Inheriting from parent", + "cs_editor.intensity": "Intensity", + "cs_editor.interior_cell": "Interior Cell", + "cs_editor.interior_only_available": "Only available in interior cells", + "cs_editor.interior_only_settings": "Interior Only Settings", + "cs_editor.interior_settings_override": "Settings added here will override feature defaults when you enter an interior cell. Values revert automatically when you exit.", + "cs_editor.json": "json", + "cs_editor.keyboard_shortcuts": "Keyboard Shortcuts:", + "cs_editor.label": "Label", + "cs_editor.light_fade": "Light Fade", + "cs_editor.light_fade_end": "Light Fade End", + "cs_editor.light_fade_start": "Light Fade Start", + "cs_editor.lighting_count": "Lighting: %d", + "cs_editor.lightning_color_label": "Lightning Color", + "cs_editor.load": "Load", + "cs_editor.load_saved_file": "Load saved file (or reset to vanilla if no file)", + "cs_editor.locked_weather_status": " [LOCKED: %s]", + "cs_editor.manual_apply_required_tooltip": "This form type is only re-read by the engine on weather reinit.\nAuto-apply is disabled - use the Apply button.", + "cs_editor.max_recent_widgets": "Max recent widgets", + "cs_editor.max_recent_widgets_tooltip": "Maximum number of recent widgets to remember", + "cs_editor.menu": "Menu", + "cs_editor.more_results": "... {} more results", + "cs_editor.most_used": "Most Used", + "cs_editor.night": "Night", + "cs_editor.night_far": "Night Far", + "cs_editor.night_max": "Night Max", + "cs_editor.night_near": "Night Near", + "cs_editor.night_power": "Night Power", + "cs_editor.no_art_objects_available": "No Art Objects available", + "cs_editor.no_effect_shaders_available": "No Effect Shaders available", + "cs_editor.no_frequent_colors": "No frequently used colors yet", + "cs_editor.no_frequent_values": "No frequently used values yet", + "cs_editor.no_interior_settings": "No interior-only settings configured.", + "cs_editor.no_lighting_data": "No lighting data available for this cell.", + "cs_editor.no_open_widgets": "No open widgets", + "cs_editor.no_recent_colors": "No recent colors", + "cs_editor.no_recent_values": "No recent values", + "cs_editor.no_widgets_open": "No widgets open", + "cs_editor.none": "None", + "cs_editor.none_filter": "None", + "cs_editor.not_interior_cell": "This cell is not an interior cell.", + "cs_editor.not_same_as_cell_lighting": "Note: This is NOT the same as cell lighting template inheritance.", + "cs_editor.num_subtextures_x": "Num Subtextures X", + "cs_editor.num_subtextures_y": "Num Subtextures Y", + "cs_editor.objects": "Objects", + "cs_editor.offset": "Offset", + "cs_editor.open": "Open", + "cs_editor.open_imagespace_edit": "Open this ImageSpace for editing", + "cs_editor.open_precipitation_edit": "Open this Precipitation for editing", + "cs_editor.open_visual_effect_edit": "Open this Visual Effect for editing", + "cs_editor.open_volumetric_edit": "Open this Volumetric Lighting for editing", + "cs_editor.open_widgets": "Open Widgets:", + "cs_editor.options": "Options", + "cs_editor.other": "Other", + "cs_editor.overwrite_files": "Overwrite Files", + "cs_editor.palette": "Palette", + "cs_editor.parameter": "Parameter", + "cs_editor.parent": "Parent", + "cs_editor.parent_cs_editor_feature": "Editor-only feature: Set a parent weather to copy settings from.", + "cs_editor.particle_density_label": "Particle Density", + "cs_editor.particle_shader": "Particle Shader", + "cs_editor.particle_size": "Particle Size", + "cs_editor.particle_texture_label": "Particle Texture", + "cs_editor.particle_type": "Particle Type", + "cs_editor.path_must_end_dds": "Path must end with '.dds'", + "cs_editor.pause_all": "Pause All", + "cs_editor.pause_time": "Pause Time", + "cs_editor.pause_time_tooltip": "Pause or resume game time progression", + "cs_editor.paused_click_resume": "Paused - click to resume", + "cs_editor.phase_function": "Phase Function", + "cs_editor.phase_function_contribution": "Phase Function Contribution", + "cs_editor.phase_function_scattering": "Phase Function Scattering", + "cs_editor.play_mode_walk": "Play Mode - Walk around normally", + "cs_editor.player_cell_unavailable": "Player cell not available.", + "cs_editor.precipitation_begin_fade_in_label": "Precipitation Begin Fade In", + "cs_editor.precipitation_end_fade_out_label": "Precipitation End Fade Out", + "cs_editor.preview_free_camera": " [ %s ] FREE CAMERA (Speed: %.0f)", + "cs_editor.preview_free_camera_locked": " [ %s ] FREE CAMERA LOCKED", + "cs_editor.preview_play_mode": " [ %s ] PLAY MODE", + "cs_editor.quick_tips": "Quick Tips:", + "cs_editor.rain": "Rain", + "cs_editor.range_factor": "Range Factor", + "cs_editor.recent": "Recent:", + "cs_editor.recent_count": "Recent: %d", + "cs_editor.recently_used": "Recently Used", + "cs_editor.record_imagespace": "ImageSpace", + "cs_editor.record_precipitation": "Precipitation", + "cs_editor.record_visual_effect": "Visual Effect", + "cs_editor.record_volumetric_lighting": "Volumetric Lighting", + "cs_editor.remove": "Remove", + "cs_editor.remove_from_palette": "Remove from palette", + "cs_editor.remove_setting": "Remove this setting", + "cs_editor.reset_speed": "Reset Speed", + "cs_editor.reset_speed_tooltip": "Reset time speed to vanilla (%.1fx)", + "cs_editor.reset_to_default": "Reset to 1.0", + "cs_editor.reset_to_global": "Reset to Global", + "cs_editor.reset_ui_scale_tooltip": "Reset UI scale to default (100%)", + "cs_editor.reset_window_layout": "Reset Window Layout", + "cs_editor.resume_time": "Resume Time", + "cs_editor.revert": "Revert", + "cs_editor.revert_to_game_values": "Revert to Game Values", + "cs_editor.revert_to_original": "Revert to original game values", + "cs_editor.rgb_color": "RGB Color", + "cs_editor.right_click_to_clear": "Right-click to clear", + "cs_editor.right_click_to_remove": "Right-click to remove", + "cs_editor.rotation_velocity": "Rotation Velocity", + "cs_editor.sampling": "Sampling", + "cs_editor.sampling_range_factor": "Sampling Range Factor", + "cs_editor.save": "Save", + "cs_editor.save_all_open_widgets": "Save All Open Widgets", + "cs_editor.save_to_file": "Save to file", + "cs_editor.save_widget": "Save {}", + "cs_editor.scattering": "Scattering", + "cs_editor.search_settings_hint": "Search settings (Ctrl+F)", + "cs_editor.select_feature": "Select Feature...", + "cs_editor.select_setting": "Select Setting...", + "cs_editor.session_history": "Session & History", + "cs_editor.settings": "Settings", + "cs_editor.shortcut_ctrl_f": "Ctrl+F: Focus search", + "cs_editor.shortcut_ctrl_s": "Ctrl+S: Save all open widgets", + "cs_editor.shortcut_ctrl_w": "Ctrl+W: Close focused widget", + "cs_editor.shortcut_enter": "Enter: Open selected widget", + "cs_editor.shortcut_esc": "Esc: Close editor", + "cs_editor.size": "Size", + "cs_editor.size_x": "Size X", + "cs_editor.size_y": "Size Y", + "cs_editor.snow": "Snow", + "cs_editor.start_rotation_range": "Start Rotation Range", + "cs_editor.status": "Status", + "cs_editor.subtextures": "Subtextures", + "cs_editor.sun_damage": "Sun Damage", + "cs_editor.tab_advanced": "Advanced", + "cs_editor.tab_basic": "Basic", + "cs_editor.tab_dalc": "DALC", + "cs_editor.tab_density": "Density", + "cs_editor.tab_fog": "Fog", + "cs_editor.tab_inheritance": "Inheritance", + "cs_editor.tab_particle": "Particle", + "cs_editor.tab_position": "Position", + "cs_editor.tab_texture": "Texture", + "cs_editor.text_buttons_tooltip": "Display action buttons as text labels instead of icons", + "cs_editor.texture_file_not_found": "Texture file not found under Data/textures/.", + "cs_editor.texture_path": "Texture Path", + "cs_editor.thunder_lightning_begin_fade_in": "Thunder Lightning Begin Fade In", + "cs_editor.thunder_lightning_end_fade_out": "Thunder Lightning End Fade Out", + "cs_editor.thunder_lightning_frequency": "Thunder Lightning Frequency", + "cs_editor.time_paused_status": " [TIME PAUSED]", + "cs_editor.time_scale_tooltip": "Adjust how fast time passes (vanilla: %.1fx)", + "cs_editor.tip_auto_apply": "Auto-Apply updates game live", + "cs_editor.tip_double_click": "Double-click to edit", + "cs_editor.tip_lock_weather": "Lock weather to prevent changes", + "cs_editor.tip_quick_filters": "Use quick filters for fast sorting", + "cs_editor.tip_right_click": "Right-click to mark status", + "cs_editor.tip_star_favorite": "Click star icon to favorite", + "cs_editor.tip_undo": "Undo button reverts recent changes (Ctrl+Z)", + "cs_editor.tod_day": "Day", + "cs_editor.tod_night": "Night", + "cs_editor.tod_sunrise": "Sunrise", + "cs_editor.tod_sunset": "Sunset", + "cs_editor.total_objects": "Total Objects:", + "cs_editor.trans_delta": "Trans Delta", + "cs_editor.transitioning": "transitioning", + "cs_editor.type": "Type", + "cs_editor.ui_scale": "UI Scale", + "cs_editor.undo_no_changes": "Undo (Ctrl+Z) - No changes to undo", + "cs_editor.undone_changes_to": "Undone changes to {}", + "cs_editor.unknown": "Unknown", + "cs_editor.unlock": "Unlock", + "cs_editor.unlock_weather": "Unlock Weather", + "cs_editor.unnamed_cell": "[Unnamed Cell]", + "cs_editor.unpause_all": "Unpause All", + "cs_editor.unsaved_changes": "(UNSAVED CHANGES)", + "cs_editor.unsaved_changes_tooltip": "Unsaved changes - click save to keep", + "cs_editor.unsupported_type": "(unsupported type)", + "cs_editor.unsupported_variable_type": "Unsupported Variable Type", + "cs_editor.unsupported_variable_type_tooltip": "This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above.", + "cs_editor.use_inherit_checkboxes": "Use 'Inherit From Parent' checkboxes to copy specific values.", + "cs_editor.use_text_buttons": "Use text buttons instead of icons", + "cs_editor.used_times": "Used {} times", + "cs_editor.user_settings": "User Settings", + "cs_editor.using_global_settings": "Using Global Settings", + "cs_editor.using_weather_specific_settings": "Using Weather-Specific Settings", + "cs_editor.value": "Value", + "cs_editor.values": "Values", + "cs_editor.values_3_plus": "(Values used 3+ times will appear here)", + "cs_editor.vanilla_speed": "Vanilla Speed", + "cs_editor.velocity": "Velocity", + "cs_editor.viewport": "Viewport", + "cs_editor.viewport_unavailable": "Viewport unavailable", + "cs_editor.viewport_unavailable_hdr": "Viewport is unavailable when HDR Display is enabled", + "cs_editor.visual_effect_begin": "Visual Effect Begin", + "cs_editor.visual_effect_end": "Visual Effect End", + "cs_editor.volume": "Volume", + "cs_editor.volumetric_lighting_label": "Volumetric Lighting:", + "cs_editor.weather_lighting_browser": "CS Editor Browser", + "cs_editor.weathers_count": "Weathers: %d", + "cs_editor.widget_type_cell_lighting": "Cell Lighting", + "cs_editor.widget_type_imagespace": "ImageSpace", + "cs_editor.widget_type_lens_flare": "Lens Flare", + "cs_editor.widget_type_lighting": "Lighting", + "cs_editor.widget_type_precipitation": "Precipitation", + "cs_editor.widget_type_visual_effect": "Visual Effect", + "cs_editor.widget_type_volumetric_lighting": "Volumetric Lighting", + "cs_editor.widget_type_weather": "Weather", + "cs_editor.wind_direction_label": "Wind Direction", + "cs_editor.wind_direction_range_label": "Wind Direction Range", + "cs_editor.wind_speed": "Wind Speed", + "cs_editor.window": "Window", + "cs_editor.xy_rotation": "XY Rotation", + "cs_editor.yes_delete": "Yes, Delete", + "cs_editor.z_rotation": "Z Rotation", + "feature.category.characters": "Characters", + "feature.category.display": "Display", + "feature.category.grass": "Grass", + "feature.category.landscape_and_textures": "Landscape & Textures", + "feature.category.lighting": "Lighting", + "feature.category.materials": "Materials", + "feature.category.other": "Other", + "feature.category.post_processing": "Post-Processing", + "feature.category.sky": "Sky", + "feature.category.utility": "Utility", + "feature.category.water": "Water", + "feature.cloud_shadows.description": "Adds realistic cloud shadows that move across the landscape, creating dynamic lighting changes as clouds pass overhead, enhancing atmospheric immersion.", + "feature.cloud_shadows.key_feature_1": "Dynamic cloud shadow projection on terrain and objects", + "feature.cloud_shadows.key_feature_2": "Configurable shadow opacity for artistic control", + "feature.cloud_shadows.key_feature_3": "Real-time shadow movement synchronized with cloud motion", + "feature.cloud_shadows.key_feature_4": "Cubemap-based shadow calculation for accurate projection", + "feature.cloud_shadows.key_feature_5": "Enhanced sky rendering integration", + "feature.cloud_shadows.name": "Cloud Shadows", + "feature.cloud_shadows.opacity": "Opacity", + "feature.cloud_shadows.opacity_tooltip": "Higher values make cloud shadows darker.", + "feature.cs_editor.accelerate_weather_change": "Accelerate Weather Change", + "feature.cs_editor.accelerate_weather_change_tooltip": "When enabled, weather changes instantly", + "feature.cs_editor.aurora": "Aurora", + "feature.cs_editor.aurora_sun": "Aurora Sun", + "feature.cs_editor.clear_all": "Clear All", + "feature.cs_editor.cloudy": "Cloudy", + "feature.cs_editor.collapse": "collapse", + "feature.cs_editor.current_weather": "Current Weather: %s", + "feature.cs_editor.current_weather_column": "Current Weather", + "feature.cs_editor.description": "Development tool for inspecting, editing, and previewing renderer-facing data in-game.", + "feature.cs_editor.effective_wind_dir": "Effective Wind Dir: %.1f° (raw - %.1f°)", + "feature.cs_editor.expand": "expand", + "feature.cs_editor.feature_weather_analysis_tooltip_0": "Weather analysis provided by: ", + "feature.cs_editor.feature_weather_analysis_tooltip_1": "Feature category: ", + "feature.cs_editor.feature_weather_analysis_tooltip_2": "Click to %s this feature's weather data", + "feature.cs_editor.filter_by_weather_type": "Filter by Weather Type:", + "feature.cs_editor.has_custom_settings": "Has Custom Settings", + "feature.cs_editor.headwind": "Headwind (wind coming toward player)", + "feature.cs_editor.key_feature_1": "Provides weather editing functionality", + "feature.cs_editor.key_feature_2": "Includes dynamic saving and loading of vanilla post processing and weather settings.", + "feature.cs_editor.key_feature_3": "Real-time editing and previewing of effects", + "feature.cs_editor.key_feature_4": "Instantly switch between any weather with immediate or gradual transitions", + "feature.cs_editor.key_feature_5": "Filter weather by type (Pleasant, Cloudy, Rainy, Snow, Aurora) for easy browsing", + "feature.cs_editor.key_feature_6": "View detailed weather information including wind, precipitation, and lightning data", + "feature.cs_editor.key_feature_7": "Color-coded weather names show all weather properties at a glance", + "feature.cs_editor.key_feature_8": "Persistent overlay window for continuous weather monitoring while playing", + "feature.cs_editor.last_weather_column": "Last Weather", + "feature.cs_editor.left_crosswind": "Left crosswind", + "feature.cs_editor.lightning_begin_fade_in": "Lightning Begin Fade-In: %.3f (raw %u)", + "feature.cs_editor.lightning_color": "Lightning Color:", + "feature.cs_editor.lightning_end_fade_out": "Lightning End Fade-Out: %.3f (raw %u)", + "feature.cs_editor.lightning_fade_info_0": "Lightning fade transition parameters:", + "feature.cs_editor.lightning_fade_info_1": "Begin Fade-In: Point where lightning starts appearing", + "feature.cs_editor.lightning_fade_info_2": "End Fade-Out: Point where lightning fully disappears", + "feature.cs_editor.lightning_fade_info_3": "Raw values: 0-255 (uint8), Normalized: 0.0-1.0", + "feature.cs_editor.lock_weather": "Lock Weather", + "feature.cs_editor.name": "CS Editor", + "feature.cs_editor.no_active_weather": "No Active Weather", + "feature.cs_editor.no_precipitation_data": "Particle Density: No precipitation data", + "feature.cs_editor.no_transition": "Transitioning From: No Transition", + "feature.cs_editor.no_weather_found": "No Weather Found", + "feature.cs_editor.none_filter": "None", + "feature.cs_editor.none_filter_tooltip_0": "Shows weathers that are not classified under any specific category.", + "feature.cs_editor.none_filter_tooltip_1": "Includes weathers with no flags or only untracked flags.", + "feature.cs_editor.none_filter_tooltip_2": "Categories tracked: Pleasant, Cloudy, Rainy, Snow, Aurora, Aurora Sun", + "feature.cs_editor.open_editor": "Open CS Editor", + "feature.cs_editor.particle_density": "Particle Density: %.3f", + "feature.cs_editor.particle_texture": "Particle Texture: %s", + "feature.cs_editor.particle_texture_none": "Particle Texture: None", + "feature.cs_editor.player_direction": "Player Direction: %.1f°", + "feature.cs_editor.pleasant": "Pleasant", + "feature.cs_editor.precip_begin_fade_in": "Precip Begin Fade-In: %.3f (raw %u)", + "feature.cs_editor.precip_end_fade_out": "Precip End Fade-Out: %.3f (raw %u)", + "feature.cs_editor.precip_fade_info_0": "Precipitation fade transition parameters:", + "feature.cs_editor.precip_fade_info_1": "Begin Fade-In: Point where precipitation starts appearing", + "feature.cs_editor.precip_fade_info_2": "End Fade-Out: Point where precipitation fully disappears", + "feature.cs_editor.precip_fade_info_3": "Raw values: 0-255 (uint8), Normalized: 0.0-1.0", + "feature.cs_editor.rainy": "Rainy", + "feature.cs_editor.reset_weather": "Reset Weather", + "feature.cs_editor.reset_weather_tooltip": "Resets weather to default", + "feature.cs_editor.right_crosswind": "Right crosswind", + "feature.cs_editor.select_all": "Select All", + "feature.cs_editor.select_weather": "Select Weather", + "feature.cs_editor.show_in_overlay": "Show in Overlay", + "feature.cs_editor.show_in_overlay_tooltip": "Opens weather details in a separate window that stays open\neven when the main menu is closed. ", + "feature.cs_editor.sky_not_available": "Sky not available", + "feature.cs_editor.sky_not_full": "Sky not in full mode", + "feature.cs_editor.sky_wind_speed": "Sky Wind Speed: %.2f", + "feature.cs_editor.sky_wind_tooltip_0": "Current active wind speed from the sky system", + "feature.cs_editor.sky_wind_tooltip_1": "This affects particle behavior and wind-based effects", + "feature.cs_editor.snow": "Snow", + "feature.cs_editor.tailwind": "Tailwind (wind behind player)", + "feature.cs_editor.thunder_freq_info_0": "Thunder frequency raw value (0-255):", + "feature.cs_editor.thunder_freq_info_1": "Known data points from Creation Kit slider:", + "feature.cs_editor.thunder_freq_info_2": "- Raw 15 = ~100% frequency (highest thunder)", + "feature.cs_editor.thunder_freq_info_3": "- Raw 76 = ~75% frequency", + "feature.cs_editor.thunder_freq_info_4": "- Raw 203 = ~20% frequency", + "feature.cs_editor.thunder_freq_info_5": "- Raw 246 = ~5% frequency", + "feature.cs_editor.thunder_freq_info_6": "- Raw 255 = ~0% frequency (lowest thunder)", + "feature.cs_editor.thunder_freq_info_7": "Range: 0-255 (unsigned 8-bit integer)", + "feature.cs_editor.thunder_freq_info_8": "Note: Creation Kit interprets this value non-linearly", + "feature.cs_editor.thunder_frequency": "Thunder Frequency: %u", + "feature.cs_editor.time_controls": "Time Controls", + "feature.cs_editor.toggle_with": "Toggle with ", + "feature.cs_editor.tooltip_editor_id": "Editor ID: %s", + "feature.cs_editor.tooltip_editor_id_2": "Editor ID: %s", + "feature.cs_editor.tooltip_flags": "Flags: %s", + "feature.cs_editor.tooltip_flags_none": "Flags: None", + "feature.cs_editor.tooltip_form_id": "Form ID: 0x%08X", + "feature.cs_editor.tooltip_form_id_2": "Form ID: 0x%08X", + "feature.cs_editor.tooltip_name": "Name: %s", + "feature.cs_editor.tooltip_weather_name": "Weather: %s", + "feature.cs_editor.transition_progress": "Transition: {:.1f}%", + "feature.cs_editor.transitioning_from": "Transitioning From: %s", + "feature.cs_editor.unknown": "Unknown", + "feature.cs_editor.unlock_weather": "Unlock Weather", + "feature.cs_editor.using_default_settings": "Using Default Settings", + "feature.cs_editor.weather": "Weather", + "feature.cs_editor.weather_controls": "Weather Controls", + "feature.cs_editor.weather_details": "Weather Details", + "feature.cs_editor.weather_information": "Weather Information", + "feature.cs_editor.weather_percentage": "Weather Percentage: %.1f%%", + "feature.cs_editor.weather_status": "Weather Status", + "feature.cs_editor.weather_wind_speed": "Weather Wind Speed: %.2f (raw %d)", + "feature.cs_editor.wind_direction": "Wind Direction: %.1f° (raw %d)", + "feature.cs_editor.wind_direction_range": "Wind Direction Range: %.1f° (raw %d)", + "feature.cs_editor.wind_direction_tooltip_0": "Wind direction from weather definition", + "feature.cs_editor.wind_speed_tooltip_0": "Wind speed from weather definition", + "feature.cs_editor.wind_vs_player": "Wind vs Player: %.1f°", + "feature.cs_editor.wind_vs_player_tooltip_0": "Wind relative to player direction:", + "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = Tailwind (wind behind player)", + "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = Crosswind (left/right)", + "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = Headwind (wind coming toward player)", + "feature.dynamic_cubemaps.advanced_vr_settings": "Advanced VR Settings", + "feature.dynamic_cubemaps.color": "Color", + "feature.dynamic_cubemaps.creator_info": "You must enable creator mode by adding the shader define CREATOR", + "feature.dynamic_cubemaps.description": "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces.", + "feature.dynamic_cubemaps.dynamic_cubemap_creator": "Dynamic Cubemap Creator", + "feature.dynamic_cubemaps.enable_creator": "Enable Creator", + "feature.dynamic_cubemaps.enable_ssr": "Enable Screen Space Reflections", + "feature.dynamic_cubemaps.enable_ssr_tooltip": "Enable Screen Space Reflections on Water", + "feature.dynamic_cubemaps.export": "Export", + "feature.dynamic_cubemaps.key_feature_1": "Real-time environment capture for realistic reflections", + "feature.dynamic_cubemaps.key_feature_2": "Dynamic cube map generation based on camera position", + "feature.dynamic_cubemaps.key_feature_3": "Enhanced water reflections with environmental details", + "feature.dynamic_cubemaps.key_feature_4": "Support for both standard and VR rendering modes", + "feature.dynamic_cubemaps.key_feature_5": "Optimized cubemap inference and irradiance calculation", + "feature.dynamic_cubemaps.name": "Dynamic Cubemaps", + "feature.dynamic_cubemaps.roughness": "Roughness", + "feature.dynamic_cubemaps.screen_space_reflections": "Screen Space Reflections", + "feature.dynamic_cubemaps.vr_restart_required": "A restart is required to enable in VR. Save Settings after enabling and restart the game.", + "feature.exp_height_fog.apply_vanilla_fade": "Apply Vanilla Fade", + "feature.exp_height_fog.apply_vanilla_fade_tooltip": "Applies vanilla fade brightness to exponential height fog.", + "feature.exp_height_fog.cubemap_mip_level": "Cubemap Mip Level", + "feature.exp_height_fog.dir_inscattering_anisotropy": "Directional Light Inscattering Anisotropy", + "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "Controls the asymmetry of inscattering via the Henyey-Greenstein phase function.\nPositive values produce forward scattering (glow around sun).\nZero is isotropic. Negative values produce back scattering.", + "feature.exp_height_fog.dir_inscattering_mul": "Directional Light Inscattering Multiplier", + "feature.exp_height_fog.disable_vanilla_fog": "Disable Vanilla Fog", + "feature.exp_height_fog.disable_vanilla_fog_tooltip": "Disables the vanilla fog entirely. Only exponential height fog will be applied.", + "feature.exp_height_fog.enable_exp_height_fog": "Enable Exponential Height Fog", + "feature.exp_height_fog.fog_density": "Fog Density", + "feature.exp_height_fog.fog_height": "Fog Height", + "feature.exp_height_fog.fog_height_falloff": "Fog Height Falloff", + "feature.exp_height_fog.fog_inscattering_color": "Fog Inscattering Color", + "feature.exp_height_fog.inscattering_cubemap_tint": "Inscattering Cubemap Tint", + "feature.exp_height_fog.original_fog_color_amount": "Original Fog Color Amount", + "feature.exp_height_fog.start_distance": "Start Distance", + "feature.exp_height_fog.sunlight_attenuation": "Sunlight Attenuation Amount", + "feature.exp_height_fog.use_dynamic_cubemaps": "Use Dynamic Cubemaps for Inscattering", + "feature.exponential_height_fog.description": "Exponential Height Fog adds a realistic fog effect that increases in density with height, enhancing atmospheric depth and immersion in the game environment.", + "feature.exponential_height_fog.key_feature_1": "Added exponential height fog effect", + "feature.exponential_height_fog.key_feature_2": "Adapted to vanilla fog settings", + "feature.exponential_height_fog.key_feature_3": "Creates atmospheric depth", + "feature.exponential_height_fog.name": "Exponential Height Fog", + "feature.extended_materials.complex_material": "Complex Material", + "feature.extended_materials.description": "Extended Materials adds advanced material effects including parallax occlusion mapping and complex material blending.\nThis feature enhances surface detail and depth perception for more realistic textures.", + "feature.extended_materials.enable_complex_material": "Enable Complex Material", + "feature.extended_materials.enable_complex_material_tooltip": "Enables support for the Complex Material specification which makes use of the environment mask. This includes parallax, as well as more realistic metals and specular reflections. May lead to some warped textures on modded content which have an invalid alpha channel in their environment mask. ", + "feature.extended_materials.enable_height_blending": "Enable Terrain Height Blending", + "feature.extended_materials.enable_height_blending_tooltip": "Enables landscape texture blending based on parallax. ", + "feature.extended_materials.enable_legacy_terrain": "Enable Legacy Terrain", + "feature.extended_materials.enable_legacy_terrain_tooltip": "Enables terrain parallax using the alpha channel of each landscape texture. Therefore, all landscape textures must support parallax for this effect to work properly. ", + "feature.extended_materials.enable_parallax": "Enable Parallax", + "feature.extended_materials.enable_parallax_tooltip": "Enables parallax on standard meshes made for parallax.", + "feature.extended_materials.enable_parallax_warping_fix": "Enable Parallax Warping Fix", + "feature.extended_materials.enable_parallax_warping_fix_tooltip": "Enables a fix reducing parallax scale on curved and smooth normal triangles.", + "feature.extended_materials.enable_shadows": "Enable Shadows", + "feature.extended_materials.enable_shadows_tooltip": "Enables cheap soft shadows when using parallax. This applies to all directional and point lights. ", + "feature.extended_materials.extend_shadows": "Extend Shadows", + "feature.extended_materials.extend_shadows_tooltip": "Extends parallax shadows beyond the range of parallax. Small performance impact.", + "feature.extended_materials.key_feature_1": "Parallax occlusion mapping for depth", + "feature.extended_materials.key_feature_2": "Complex material blending", + "feature.extended_materials.key_feature_3": "Terrain heightmap support", + "feature.extended_materials.key_feature_4": "Parallax shadows", + "feature.extended_materials.key_feature_5": "Height-based texture blending", + "feature.extended_materials.name": "Extended Materials", + "feature.extended_materials.parallax": "Parallax", + "feature.extended_materials.soft_shadows": "Approximate Soft Shadows", + "feature.extended_translucency.alpha_mode_anisotropic_fabric": "3 - Anisotropic Fabric", + "feature.extended_translucency.alpha_mode_disabled": "0 - Disabled", + "feature.extended_translucency.alpha_mode_isotropic_fabric": "2 - Isotropic Fabric, Glass, ...", + "feature.extended_translucency.alpha_mode_rim_edge": "1 - Rim Edge", + "feature.extended_translucency.blend_weight": "Blend Weight", + "feature.extended_translucency.blend_weight_tooltip": "Control the blend weight of the effect applied to the final result.", + "feature.extended_translucency.default_material_model": "Default Material Model", + "feature.extended_translucency.default_material_model_tooltip": "Anisotropic translucency will adjust the opacity based on your view angle to the translucent surface.\n - Disabled: No anisotropic translucency, flat alpha.\n - Rim Edge: Naive rim light effect with no physics model, the edge of the geometry is always opaque even its full transparent.\n - Isotropic Fabric: Imaginary fabric weaved from threads in one direction, respect normal map, also works well for layer of glass panels.\n - Anisotropic Fabric: Common fabric weaved from tangent and birnormal direction, ignores normal map.\n", + "feature.extended_translucency.description": "Extended Translucency provides realistic rendering of thin fabric and other translucent materials.\nThis feature supports multiple material models for different types of translucent surfaces.", + "feature.extended_translucency.key_feature_1": "Multiple translucency material models (rim edge, isotropic/anisotropic fabric)", + "feature.extended_translucency.key_feature_2": "Realistic fabric translucency with directional light transmission", + "feature.extended_translucency.key_feature_3": "Per-material override support via NIF extra data", + "feature.extended_translucency.key_feature_4": "Configurable transparency and softness controls", + "feature.extended_translucency.key_feature_5": "Performance-optimized translucency calculations", + "feature.extended_translucency.name": "Extended Translucency", + "feature.extended_translucency.skinned_mesh_only": "Skinned Mesh Only", + "feature.extended_translucency.skinned_mesh_only_tooltip": "Control if this effect should only apply to skinned mesh. Check this option if you are seeing undesired effects on random objects.", + "feature.extended_translucency.softness": "Softness", + "feature.extended_translucency.softness_tooltip": "Control the softness of the alpha increase, increase the softness reduce the increased amount of alpha.", + "feature.extended_translucency.translucent_material": "Translucent Material", + "feature.extended_translucency.transparency_increase": "Transparency Increase", + "feature.extended_translucency.transparency_increase_tooltip": "Translucent material will make the material more opaque on average, which could be different from the intent. Reduce the alpha to counter this effect and increase the dynamic range of the output.", + "feature.grass_collision.description": "Enables dynamic grass interactions where grass bends and moves in response to actors walking through it, creating more immersive environmental reactions.", + "feature.grass_collision.enable": "Enable Grass Collision", + "feature.grass_collision.grass_collision": "Grass Collision", + "feature.grass_collision.key_feature_1": "Real-time grass deformation from actor movement", + "feature.grass_collision.key_feature_2": "Collision detection for up to 256 simultaneous interactions", + "feature.grass_collision.key_feature_3": "Dynamic tracking of actor positions for grass response", + "feature.grass_collision.key_feature_4": "Performance-optimized collision calculation", + "feature.grass_collision.key_feature_5": "Seamless integration with existing grass rendering", + "feature.grass_collision.name": "Grass Collision", + "feature.grass_lighting.basic_grass": "Basic Grass", + "feature.grass_lighting.brightness": "Brightness", + "feature.grass_lighting.brightness_tooltip": "Darkens the grass textures to look better with the new lighting", + "feature.grass_lighting.complex_grass": "Complex Grass", + "feature.grass_lighting.description": "Grass Lighting enhances grass rendering with improved lighting, specularity, and subsurface scattering.\nThis makes grass appear more natural and responsive to lighting conditions.", + "feature.grass_lighting.detection_header": "Complex Grass Detection", + "feature.grass_lighting.detection_threshold": "Detection Threshold", + "feature.grass_lighting.detection_threshold_tooltip": "Threshold for detecting complex grass textures. Lower values are more strict.", + "feature.grass_lighting.effects": "Effects", + "feature.grass_lighting.glossiness": "Glossiness", + "feature.grass_lighting.glossiness_tooltip": "Specular highlight glossiness.", + "feature.grass_lighting.key_feature_1": "Enhanced grass lighting model", + "feature.grass_lighting.key_feature_2": "Specular highlights on grass", + "feature.grass_lighting.key_feature_3": "Subsurface scattering effects", + "feature.grass_lighting.key_feature_4": "Improved grass visual quality", + "feature.grass_lighting.key_feature_5": "Configurable material properties", + "feature.grass_lighting.lighting": "Lighting", + "feature.grass_lighting.name": "Grass Lighting", + "feature.grass_lighting.override_complex": "Override Complex Grass Lighting Settings", + "feature.grass_lighting.override_complex_tooltip": "Override the settings set by the grass mesh author. Complex grass authors can define the brightness for their grass meshes. However, some authors may not account for the extra lights available from Community Shaders. This option will treat their grass settings like non-complex grass. This was the default in Community Shaders < 0.7.0", + "feature.grass_lighting.specular_desc": "Specular highlights for complex grass", + "feature.grass_lighting.specular_strength": "Specular Strength", + "feature.grass_lighting.specular_strength_tooltip": "Specular highlight strength.", + "feature.grass_lighting.sss_amount": "SSS Amount", + "feature.grass_lighting.sss_tooltip": "Subsurface Scattering (SSS) amount. Soft lighting controls how evenly lit an object is. Back lighting illuminates the back face of an object. Combined to model the transport of light through the surface.", + "feature.hair_specular.description": "Provides better hair shading with realistic specular highlights and tangent-based light interaction for more lifelike hair appearance.", + "feature.hair_specular.diffuse_multiplier": "Diffuse Multiplier", + "feature.hair_specular.enable_self_shadow": "Enable Screen-Space Self Shadow", + "feature.hair_specular.enable_self_shadow_tooltip": "Enables screen-space self-shadowing for hair.\nMarschner hair model might have overly bright transmission without self-shadowing.\n", + "feature.hair_specular.enable_tangent_shift": "Enable Tangent Shift", + "feature.hair_specular.enable_tangent_shift_tooltip": "Enables the use of a tangent shift texture to vary specular highlights across hair strands.\nResult may vary based on the hair model used.\n", + "feature.hair_specular.enabled": "Enabled", + "feature.hair_specular.glossiness": "Glossiness", + "feature.hair_specular.glossiness_tooltip": "Controls the glossiness of the hair.\nGlossiness in Kajiya-Kay mode maps to the specular exponent.\nIn Marschner mode, it controls the roughness of the hair surface.\n", + "feature.hair_specular.hair_base_color_multiplier": "Hair Base Color Multiplier", + "feature.hair_specular.hair_mode": "Hair Mode", + "feature.hair_specular.hair_mode_tooltip": "Select the hair shading model to use.\nKajiya-Kay is an empirical model that simulates hair specular highlights.\nMarschner is a more physically-based model that simulates hair light interaction.\nBoth models are anisotropic and support tangent-based shading.\nWithout self-shadowing, Marschner may look overly bright because of transmission.\n", + "feature.hair_specular.hair_saturation": "Hair Saturation", + "feature.hair_specular.indirect_diffuse_multiplier": "Indirect Diffuse Multiplier", + "feature.hair_specular.indirect_specular_multiplier": "Indirect Specular Multiplier", + "feature.hair_specular.key_feature_1": "Realistic hair specular highlights", + "feature.hair_specular.key_feature_2": "Enhanced hair glossiness and saturation controls", + "feature.hair_specular.key_feature_3": "Separate specular and diffuse lighting multipliers", + "feature.hair_specular.key_feature_4": "Tangent shift texture support for varied hair highlights", + "feature.hair_specular.name": "Hair Specular", + "feature.hair_specular.primary_tangent_shift": "Primary Specular Tangent Shift", + "feature.hair_specular.secondary_tangent_shift": "Secondary Specular Tangent Shift", + "feature.hair_specular.self_shadow_exponent": "Self Shadow Exponent", + "feature.hair_specular.self_shadow_scale": "Self Shadow Scale", + "feature.hair_specular.self_shadow_strength": "Self Shadow Strength", + "feature.hair_specular.specular_multiplier": "Specular Multiplier", + "feature.hair_specular.transmission": "Transmission", + "feature.hdr_display.advanced": "Advanced", + "feature.hdr_display.advanced_tooltip_enable_windows_hdr": "Enable Windows HDR instead of forcing it here.", + "feature.hdr_display.advanced_tooltip_force_enable": "Force enable HDR even without detection (not recommended).", + "feature.hdr_display.cancel": "Cancel", + "feature.hdr_display.capable_display_windows_hdr_off": "HDR Capable Display (Windows HDR is off)", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_0": "Your monitor supports HDR, but Windows HDR is currently disabled.", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_1": "Enable HDR in Windows Display Settings to allow auto-detection.", + "feature.hdr_display.description": "Real High Dynamic Range output for HDR displays.", + "feature.hdr_display.display_detected": "HDR Display Detected", + "feature.hdr_display.display_reports_max_nits": "Display reports: %.0f nits max", + "feature.hdr_display.display_reports_max_nits_tooltip_0": "Reported by OS/driver (DXGI MaxLuminance), not a direct meter reading.", + "feature.hdr_display.display_reports_max_nits_tooltip_1": "It may be EDID metadata and can differ from real highlight peak output.", + "feature.hdr_display.display_reports_max_nits_tooltip_2": "Treat this as a starting point and tune Peak Brightness as needed.", + "feature.hdr_display.dont_show_again": "Don't show me this again", + "feature.hdr_display.enable_hdr": "Enable HDR", + "feature.hdr_display.enable_hdr_tooltip": "Enable HDR output. Matches vanilla visuals with extended dynamic range.", + "feature.hdr_display.enable_hdr_tooltip_not_detected": "HDR display not detected. Use Advanced button to override.", + "feature.hdr_display.enable_hdr_tooltip_windows_off": "Monitor supports HDR but Windows HDR is off. Enable HDR in Windows Display Settings, then restart the game.", + "feature.hdr_display.enabled_without_detected_display": "HDR is enabled but no HDR display was detected.", + "feature.hdr_display.exclusive_fullscreen_warning": "WARNING: Exclusive Fullscreen detected.", + "feature.hdr_display.exclusive_fullscreen_warning_detail": "HDR is not compatible with Exclusive Fullscreen and may not work correctly. Switch to Borderless Windowed mode for proper HDR support.", + "feature.hdr_display.force_enable_hdr": "Force Enable HDR", + "feature.hdr_display.force_enable_hdr_confirm": "Only proceed if you have an HDR-capable display that was not detected correctly.", + "feature.hdr_display.force_enable_hdr_detected_warning": "HDR was not detected on your monitor.", + "feature.hdr_display.force_enable_hdr_sdr_warning": "The game will look VERY WRONG on an SDR (standard) display.", + "feature.hdr_display.force_enable_hdr_warning": "WARNING: Force Enable HDR", + "feature.hdr_display.key_feature_1": "HDR10 output support (10-bit) with upgraded HDR buffers (16-Bit), and fully unclamped rendering pipeline for true HDR values.", + "feature.hdr_display.key_feature_2": "HDR-aware tonemapping based on Skyrim's ISHDR path (Reinhard/Hejl-Burgess-Dawson), preserving the vanilla look while improving highlight handling on HDR displays.", + "feature.hdr_display.key_feature_3": "Configurable paper white and peak brightness.", + "feature.hdr_display.name": "HDR Display", + "feature.hdr_display.paper_white_nits": "Paper White (nits)", + "feature.hdr_display.paper_white_tooltip_0": "How bright SDR white appears on your HDR display.", + "feature.hdr_display.paper_white_tooltip_1": "203 nits is the ITU BT.2408 reference. Increase for a brighter image.", + "feature.hdr_display.peak_brightness_nits": "Peak Brightness (nits)", + "feature.hdr_display.peak_brightness_tooltip_0": "Maximum brightness your display can produce.", + "feature.hdr_display.peak_brightness_tooltip_1": "Set to match your display's actual peak brightness.", + "feature.hdr_display.sdr_display_not_detected": "SDR Display (HDR not detected)", + "feature.hdr_display.ui_brightness_multiplier": "UI Brightness Multiplier", + "feature.hdr_display.ui_brightness_multiplier_tooltip_0": "UI brightness = Paper White x this multiplier in HDR mode.", + "feature.hdr_display.ui_brightness_multiplier_tooltip_1": "1.00x = UI renders at Paper White brightness. Higher values make UI brighter relative to scene content.", + "feature.hdr_display.ui_brightness_multiplier_tooltip_2": "Note: Main menu and loading screens always render at Paper White brightness.", + "feature.hdr_display.warning_popup_title": "HDR Warning", + "feature.ibl.dalc_amount": "DALC Amount", + "feature.ibl.dalc_amount_tooltip": "Blends the IBL brightness toward the game's vanilla ambient (DALC) level.\n0 = no matching (pure IBL brightness), 1 = fully matched to vanilla ambient.", + "feature.ibl.dalc_mode": "DALC Mode", + "feature.ibl.dalc_mode_color_ratio": "Color Ratio", + "feature.ibl.dalc_mode_dalc_plus_sky": "DALC + Sky", + "feature.ibl.dalc_mode_dalc_plus_sky_directional": "DALC + Sky (Directional)", + "feature.ibl.dalc_mode_luminance_ratio": "Luminance Ratio", + "feature.ibl.dalc_mode_tooltip": "How the DALC-to-IBL brightness ratio is computed:\nLuminance Ratio: Scalar ratio from overall luminance (loses DALC color tint).\nColor Ratio: Per-channel ratio (preserves DALC color tint).\nDALC + Sky: Uses vanilla ambient as base, sky IBL on top. Skylighting only affects sky.\nDALC + Sky (Directional): Same, but Skylighting also dims vanilla ambient per-direction.", + "feature.ibl.description": "Replaces the game's ambient lighting with physically-based IBL derived from cubemap spherical harmonics.", + "feature.ibl.disable_in_interiors": "Disable in interiors", + "feature.ibl.disable_in_interiors_tooltip": "Disables IBL in interior cells.", + "feature.ibl.enable_ibl": "Enable IBL", + "feature.ibl.enable_ibl_tooltip": "Toggle IBL. When enabled, ambient lighting is derived from cubemap spherical harmonics instead of the vanilla system.", + "feature.ibl.env_ibl_saturation": "Env IBL Saturation", + "feature.ibl.env_ibl_saturation_tooltip": "Color saturation of the environment IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color.", + "feature.ibl.env_ibl_scale": "Env IBL Scale", + "feature.ibl.env_ibl_scale_tooltip": "Intensity multiplier for the environment IBL (from Dynamic Cubemaps).\nControls how strongly the surrounding environment contributes to ambient lighting.", + "feature.ibl.fog_mix": "Fog Mix", + "feature.ibl.fog_mix_tooltip": "Blends the fog color toward the IBL ambient color.\n0 = vanilla fog, 1 = fog fully tinted by IBL.", + "feature.ibl.key_feature_1": "Projects environment and sky cubemaps into spherical harmonics (SH) for irradiance", + "feature.ibl.key_feature_2": "Dual IBL sources: environment cubemap (Dynamic Cubemaps) and Skyrim's native sky reflections cubemap", + "feature.ibl.key_feature_3": "DALC brightness matching to keep IBL consistent with the game's ambient light levels", + "feature.ibl.key_feature_4": "Configurable per-source intensity, saturation, fog mixing, and per-weather overrides", + "feature.ibl.key_feature_5": "Static IBL fallback textures for out-of-world objects (e.g. inventory items)", + "feature.ibl.name": "Image Based Lighting", + "feature.ibl.preserve_fog_luminance": "Preserve Fog Luminance", + "feature.ibl.preserve_fog_luminance_tooltip": "When Fog Mix is active, rescales the IBL-tinted fog to keep the original fog brightness.\nPrevents fog from becoming too bright or too dark.", + "feature.ibl.sky_ibl_saturation": "Sky IBL Saturation", + "feature.ibl.sky_ibl_saturation_tooltip": "Color saturation of the sky IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color.", + "feature.ibl.sky_ibl_scale": "Sky IBL Scale", + "feature.ibl.sky_ibl_scale_tooltip": "Intensity multiplier for the sky IBL (from the game's native reflections cubemap).\nControls how strongly the sky contributes to ambient lighting.", + "feature.ibl.use_static_ibl": "Use Static IBL For Out-of-World Objects", + "feature.ibl.use_static_ibl_tooltip": "Uses pre-baked static IBL cubemap textures for objects rendered outside the game world (e.g. inventory items, loading screens).", + "feature.interior_sun.description": "Allows for the sun and moon to cast light and shadows into interior spaces.", + "feature.interior_sun.force_double_sided": "Force Double-Sided Rendering", + "feature.interior_sun.force_double_sided_tooltip": "Disables backface culling during sun shadowmap rendering in interiors. Will prevent most light leaking through unmasked/unprepared interiors at a small performance cost. ", + "feature.interior_sun.interior_shadow_distance": "Interior Shadow Distance", + "feature.interior_sun.interior_shadow_distance_tooltip": "Sets the distance shadows are rendered at in interiors. Lower values provide higher quality shadows and improved performance but may cause distant interior spaces to light up incorrectly. ", + "feature.interior_sun.key_feature_1": "Functions only for explicitly enabled interiors", + "feature.interior_sun.key_feature_2": "Utilizes existing sun, moon, and weather systems", + "feature.interior_sun.key_feature_3": "Includes an option to force double-sided rendering for unprepared interiors", + "feature.interior_sun.key_feature_4": "Fixes geometry culling issues that cause light leakage", + "feature.interior_sun.name": "Interior Sun", + "feature.inverse_square_lighting.description": "Implements an additional inverse square falloff for lighting which allows for a more physically accurate and realistic looking light attenuation.", + "feature.inverse_square_lighting.key_feature_1": "Automatic light radius calculation based on intensity", + "feature.inverse_square_lighting.key_feature_2": "Lights smoothly fade out at a configurable cutoff, solving the infinite distance problem", + "feature.inverse_square_lighting.key_feature_3": "Does not modify any existing lighting", + "feature.inverse_square_lighting.key_feature_4": "Requires the use of mods with lights enabled for inverse square falloff.", + "feature.inverse_square_lighting.key_feature_5": "Full integration with Light Placer", + "feature.inverse_square_lighting.name": "Inverse Square Lighting", + "feature.key_features": "Key features:", + "feature.light_editor.active_shadow_lights": "Active Shadow Lights: %u", + "feature.light_editor.base_object": "Base Object: 0x%08X | %s", + "feature.light_editor.cell": "Cell: %s", + "feature.light_editor.color": "Color", + "feature.light_editor.cutoff": "Cutoff", + "feature.light_editor.disable_inverse_square_falloff_lights": "Disable Inverse Square Falloff Lights", + "feature.light_editor.disable_regular_falloff_lights": "Disable Regular Falloff Lights", + "feature.light_editor.dynamic": "Dynamic", + "feature.light_editor.filter_by": "Filter By", + "feature.light_editor.flicker": "Flicker", + "feature.light_editor.flicker_slow": "Flicker Slow", + "feature.light_editor.hemi_shadow": "Hemi Shadow", + "feature.light_editor.intensity": "Intensity", + "feature.light_editor.inverse_square_light": "Inverse Square Light", + "feature.light_editor.ligh": "LIGH: 0x%08X | %s", + "feature.light_editor.light_flags": "Light Flags", + "feature.light_editor.lights": "Lights", + "feature.light_editor.linear_light": "Linear Light", + "feature.light_editor.memory_address": "Memory Address: %p", + "feature.light_editor.negative": "Negative", + "feature.light_editor.ni_light_name": "NiLight Name: %s", + "feature.light_editor.omni_shadow": "Omni Shadow", + "feature.light_editor.owner": "Owner: 0x%08X | %s", + "feature.light_editor.owner_last_edited_by": "Owner last edited by: %s", + "feature.light_editor.portal_strict": "Portal Strict", + "feature.light_editor.position_format": "X: %.2f, Y: %.2f, Z: %.2f", + "feature.light_editor.position_offset": "Position Offset", + "feature.light_editor.pulse": "Pulse", + "feature.light_editor.pulse_slow": "Pulse Slow", + "feature.light_editor.radius": "Radius", + "feature.light_editor.revert_changes": "Revert Changes", + "feature.light_editor.save_to_light_placer": "Save to Light Placer", + "feature.light_editor.save_to_light_placer_tooltip": "Save current settings to the Light Placer JSON.", + "feature.light_editor.select_a_light": "Select a light", + "feature.light_editor.shadows_only": "Shadows Only", + "feature.light_editor.shadows_only_tooltip": "Only show lights with HemiShadow or OmniShadow flags.", + "feature.light_editor.size": "Size", + "feature.light_editor.sort_by": "Sort By", + "feature.light_editor.spotlight_not_applicable": "Spotlight: ISL light type flags not applicable", + "feature.light_editor.total_lights": "Total Lights: %u", + "feature.light_limit_fix.debug": "Debug", + "feature.light_limit_fix.debug_feature_enabled": "DEBUG FEATURE - LIGHT LIMIT VISUALISATION ENABLED", + "feature.light_limit_fix.description": "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes.\nThis dramatically improves lighting quality and enables more realistic illumination scenarios.", + "feature.light_limit_fix.enable_lights_vis": "Enable Lights Visualisation", + "feature.light_limit_fix.enable_lights_vis_tooltip": "Enables visualization of the light limit\n", + "feature.light_limit_fix.key_feature_1": "Removes 4-light limit", + "feature.light_limit_fix.key_feature_2": "Unlimited dynamic lights", + "feature.light_limit_fix.key_feature_3": "Improved lighting quality", + "feature.light_limit_fix.key_feature_4": "Enhanced visual realism", + "feature.light_limit_fix.key_feature_5": "Enhanced visual realism", + "feature.light_limit_fix.light_limit_vis": "Light Limit Visualization", + "feature.light_limit_fix.lights_vis_mode": "Lights Visualisation Mode", + "feature.light_limit_fix.lights_vis_mode_tooltip": " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n - Visualise the number of strict lights.\n - Visualise the number of clustered lights.\n - Visualize the Shadow Mask.\n", + "feature.light_limit_fix.name": "Light Limit Fix", + "feature.light_limit_fix.statistics": "Statistics", + "feature.linear_lighting.ambient_gamma": "Ambient Gamma", + "feature.linear_lighting.ambient_multiplier": "Ambient Multiplier", + "feature.linear_lighting.blood_effects_multiplier": "Blood Effects Multiplier", + "feature.linear_lighting.color_gamma": "Color Gamma", + "feature.linear_lighting.deferred_effects_multiplier": "Deferred Effects Multiplier", + "feature.linear_lighting.description": "Linear Lighting does internal color space conversion to improve lighting calculation accuracy.", + "feature.linear_lighting.directional_light_multiplier": "Directional Light Multiplier", + "feature.linear_lighting.effect_gamma": "Effect Gamma", + "feature.linear_lighting.effect_lighting_multiplier": "Effect Lighting Multiplier", + "feature.linear_lighting.effect_transparency_gamma": "Effect Transparency Gamma", + "feature.linear_lighting.effects": "Effects", + "feature.linear_lighting.emissive_color_gamma": "Emissive Color Gamma", + "feature.linear_lighting.emissive_color_multiplier": "Emissive Color Multiplier", + "feature.linear_lighting.enable": "Enable Linear Lighting", + "feature.linear_lighting.fog_gamma": "Fog Gamma", + "feature.linear_lighting.fog_transparency_gamma": "Fog Transparency Gamma", + "feature.linear_lighting.gamma_settings": "Gamma Settings", + "feature.linear_lighting.glowmap_gamma": "Glowmap Gamma", + "feature.linear_lighting.glowmap_multiplier": "Glowmap Multiplier", + "feature.linear_lighting.key_feature_1": "Customizable gamma correction", + "feature.linear_lighting.key_feature_2": "Corrects lighting calculations", + "feature.linear_lighting.key_feature_3": "Makes PBR really work", + "feature.linear_lighting.light_gamma": "Light Gamma", + "feature.linear_lighting.membrane_effects_multiplier": "Membrane Effects Multiplier", + "feature.linear_lighting.multipliers": "Multipliers", + "feature.linear_lighting.name": "Linear Lighting", + "feature.linear_lighting.other_effects_multiplier": "Other Effects Multiplier", + "feature.linear_lighting.point_light_multiplier": "Point Light Multiplier", + "feature.linear_lighting.projected_effects_multiplier": "Projected Effects Multiplier", + "feature.linear_lighting.sky_gamma": "Sky Gamma", + "feature.linear_lighting.tab_advanced": "Advanced", + "feature.linear_lighting.tab_general": "General", + "feature.linear_lighting.vanilla_diffuse_color_multiplier": "Vanilla Diffuse Color Multiplier", + "feature.linear_lighting.vl_gamma": "Volumetric Lighting Gamma", + "feature.linear_lighting.water_gamma": "Water Gamma", + "feature.lod_blending.description": "Provides seamless visual transitions between Level of Detail (LOD) objects and full-detail objects, eliminating harsh transitions and creating smooth visual continuity.", + "feature.lod_blending.disable_terrain_vertex_colors": "Disable Terrain Vertex Colors", + "feature.lod_blending.disable_terrain_vertex_colors_tooltip": "Disables vertex coloring on nearby terrain. Best combined with terrain LOD generated in xLODGen with Vertex Color Intensity set to 0.", + "feature.lod_blending.key_feature_1": "Smooth LOD object brightness blending", + "feature.lod_blending.key_feature_2": "Enhanced terrain LOD appearance matching", + "feature.lod_blending.key_feature_3": "Snow-specific LOD brightness adjustment", + "feature.lod_blending.key_feature_4": "Optional terrain vertex color modification", + "feature.lod_blending.key_feature_5": "Seamless transition between detail levels", + "feature.lod_blending.lod_object_brightness": "LOD Object Brightness", + "feature.lod_blending.lod_object_gamma": "LOD Object Gamma", + "feature.lod_blending.lod_object_snow_brightness": "LOD Object Snow Brightness", + "feature.lod_blending.lod_object_snow_gamma": "LOD Object Snow Gamma", + "feature.lod_blending.lod_terrain_brightness": "LOD Terrain Brightness", + "feature.lod_blending.lod_terrain_gamma": "LOD Terrain Gamma", + "feature.lod_blending.name": "LOD Blending", + "feature.perf_overlay.appearance": "Appearance", + "feature.perf_overlay.bg_opacity": "Background Opacity", + "feature.perf_overlay.clear_test_data": "Clear Test Data", + "feature.perf_overlay.display_options": "Display Options", + "feature.perf_overlay.fps": "FPS:", + "feature.perf_overlay.frame_history_size": "Frame History Size", + "feature.perf_overlay.overlay_title": "Performance Overlay", + "feature.perf_overlay.position": "Position:", + "feature.perf_overlay.post_fg_calculated": "Post-FG: Calculated timing (2x Pre-FG)", + "feature.perf_overlay.post_fg_fps": "Post-FG FPS:", + "feature.perf_overlay.post_fg_graph_tooltip": "FSR Frame Generation uses calculated timing data (2x Pre-FG).\nDLSS Frame Generation provides measured timing data.", + "feature.perf_overlay.raw_fps": "Raw FPS:", + "feature.perf_overlay.reset_position": "Reset Position", + "feature.perf_overlay.restore_defaults": "Restore Defaults", + "feature.perf_overlay.restore_defaults_tooltip": "Restores Performance Overlay settings to defaults, including graphs, appearance, and update intervals.", + "feature.perf_overlay.show_border": "Show Border", + "feature.perf_overlay.show_cs_passes": "Show CS Render Passes", + "feature.perf_overlay.show_draw_calls": "Show Draw Calls", + "feature.perf_overlay.show_fps": "Show FPS Counter", + "feature.perf_overlay.show_frametime_graph": "Show Frametime Graph", + "feature.perf_overlay.show_in_overlay": "Show in Overlay", + "feature.perf_overlay.show_in_overlay_tooltip": "Opens performance overlay in a separate window that stays open\neven when the main menu is closed. ", + "feature.perf_overlay.show_post_fg_graph": "Show Post-FG Frametime Graph", + "feature.perf_overlay.show_pre_fg_graph": "Show Pre-FG Frametime Graph", + "feature.perf_overlay.show_vram": "Show VRAM Usage", + "feature.perf_overlay.text_size": "Text Size", + "feature.perf_overlay.toggle_with": "Toggle with ", + "feature.perf_overlay.update_interval": "Update Interval", + "feature.perf_overlay.vram_not_available": "VRAM Usage: Not available", + "feature.perf_overlay.vram_usage": "VRAM Usage:", + "feature.performance_overlay.description": "Real-time performance monitoring system that displays FPS, frame times, draw calls, VRAM usage, and detailed shader performance analysis.", + "feature.performance_overlay.key_feature_1": "Real-time FPS and frame time monitoring with configurable update intervals", + "feature.performance_overlay.key_feature_2": "Interactive draw call analysis with per-shader type performance breakdown", + "feature.performance_overlay.key_feature_3": "VRAM usage monitoring with visual progress bars", + "feature.performance_overlay.key_feature_4": "Frame time graphs for pre and post-frame generation analysis", + "feature.performance_overlay.key_feature_5": "A/B testing support for performance comparison between configurations", + "feature.performance_overlay.key_feature_6": "Color-coded performance metrics with customizable thresholds", + "feature.performance_overlay.key_feature_7": "Movable overlay window with persistent positioning", + "feature.performance_overlay.name": "Performance Overlay", + "feature.render_doc.description": "In-application RenderDoc capture support and convenience UI.", + "feature.render_doc.key_feature_1": "Attach comments to captures that appear in RenderDoc UI", + "feature.render_doc.key_feature_2": "Open captures folder", + "feature.render_doc.key_feature_3": "Capture file management", + "feature.render_doc.name": "RenderDoc", + "feature.renderdoc.cancel": "Cancel", + "feature.renderdoc.capture_active": "RenderDoc capture is active.", + "feature.renderdoc.capture_control": "Capture Control", + "feature.renderdoc.capture_control_tooltip": "Manual capture creation and basic controls", + "feature.renderdoc.capture_dir": "Capture Directory: %s", + "feature.renderdoc.capture_dir_tooltip": "Right-click to copy the directory path.", + "feature.renderdoc.capture_files": "Capture Files", + "feature.renderdoc.capture_files_tooltip": "View and manage individual capture files", + "feature.renderdoc.capture_frames": "Capture Frames", + "feature.renderdoc.capture_frames_tooltip": "Number of consecutive frames to capture. 1 uses a normal RenderDoc capture; higher values use TriggerMultiFrameCapture.", + "feature.renderdoc.capture_size": "Capture Size", + "feature.renderdoc.capture_size_tooltip": "Total size of all capture files in the captures directory", + "feature.renderdoc.clear_all_captures": "Clear All Captures", + "feature.renderdoc.col_created": "Created", + "feature.renderdoc.col_filename": "Filename", + "feature.renderdoc.col_size": "Size", + "feature.renderdoc.comments_hint": "Additional comments for next capture (optional)", + "feature.renderdoc.comments_tooltip": "Additional comments will be appended to automatic metadata and embedded in the .rdc file", + "feature.renderdoc.confirm_delete": "Are you sure you want to delete all capture files?", + "feature.renderdoc.copy_dir_path": "Copy Directory Path", + "feature.renderdoc.create_capture": "Create Capture", + "feature.renderdoc.delete_size": "This will permanently remove %u MB of capture data.", + "feature.renderdoc.disk_usage": "Disk Usage", + "feature.renderdoc.disk_usage_tooltip": "Monitor capture storage usage", + "feature.renderdoc.double_click_hint": "Double-click a filename to open the capture file", + "feature.renderdoc.enable_capture": "Enable RenderDoc Capture", + "feature.renderdoc.enable_capture_tooltip": "Enable RenderDoc frame capture for providing debug captures to the Community Shaders team.", + "feature.renderdoc.enable_capture_tooltip2": "Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled.", + "feature.renderdoc.hover_hint": "Hover over filenames for file details", + "feature.renderdoc.no_files": "No capture files found.", + "feature.renderdoc.not_enough_space": "Not enough free disk space to create a capture.", + "feature.renderdoc.ok": "OK", + "feature.renderdoc.open_capture_dir": "Open Capture Directory", + "feature.renderdoc.refresh_list": "Refresh List", + "feature.renderdoc.restart_to_disable": "Requires restart to disable RenderDoc capture, performance will be severely impacted.", + "feature.renderdoc.restart_to_enable": "Requires restart to enable RenderDoc capture.", + "feature.renderdoc.space_required": "At least {} MB of free space is required.", + "feature.renderdoc.yes_delete": "Yes, Delete All", + "feature.screen_space_gi.description": "Screen Space Global Illumination adds realistic indirect lighting and ambient occlusion to the game. This technique simulates how light bounces off surfaces to illuminate other objects naturally.", + "feature.screen_space_gi.key_feature_1": "Realistic indirect lighting", + "feature.screen_space_gi.key_feature_2": "Enhanced ambient occlusion", + "feature.screen_space_gi.key_feature_3": "Improved visual depth and atmosphere", + "feature.screen_space_gi.key_feature_4": "Temporal denoising for smooth results", + "feature.screen_space_gi.key_feature_5": "Configurable quality and performance settings", + "feature.screen_space_gi.name": "Screen Space GI", + "feature.screen_space_gi.vr_warning": "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects.", + "feature.screen_space_shadows.bilinear_threshold": "Bilinear Threshold", + "feature.screen_space_shadows.bilinear_threshold_tooltip": "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.", + "feature.screen_space_shadows.description": "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\nThis technique adds fine-detail shadows that traditional shadow mapping might miss.", + "feature.screen_space_shadows.enable": "Enable", + "feature.screen_space_shadows.enable_tooltip": "Enable screen-space contact shadows from the sun/moon direction.", + "feature.screen_space_shadows.general": "General", + "feature.screen_space_shadows.key_feature_1": "Enhanced contact shadows", + "feature.screen_space_shadows.key_feature_2": "Improved shadow detail", + "feature.screen_space_shadows.key_feature_3": "Better shadow accuracy", + "feature.screen_space_shadows.key_feature_4": "Fine-scale shadow effects", + "feature.screen_space_shadows.key_feature_5": "Configurable shadow contrast", + "feature.screen_space_shadows.name": "Screen Space Shadows", + "feature.screen_space_shadows.sample_count": "Sample Count Multiplier", + "feature.screen_space_shadows.sample_count_tooltip": "Multiplier for shadow ray sample count. Higher values increase shadow reach at the cost of performance. Adapts to render resolution.", + "feature.screen_space_shadows.shadow_contrast": "Shadow Contrast", + "feature.screen_space_shadows.shadow_contrast_tooltip": "Contrast boost for the shadow transition. Higher values produce harder shadow edges.", + "feature.screen_space_shadows.surface_thickness": "Surface Thickness", + "feature.screen_space_shadows.surface_thickness_tooltip": "Assumed thickness of surfaces for shadow detection. Lower values produce thinner, more precise shadows.", + "feature.screen_space_shadows.vr_stereo_sync": "VR Stereo Sync", + "feature.screen_space_shadows.vr_stereo_sync_tooltip": "Synchronizes shadow data between left and right eyes via bilateral reprojection and applies a depth-weighted blur to reduce per-eye noise. Uses min-blend so if either eye detects an occluder, the shadow is preserved. ", + "feature.screenshot.apply_crop": "Apply crop", + "feature.screenshot.async_note": "Capture and save run asynchronously without stalling the game.", + "feature.screenshot.crop": "Crop", + "feature.screenshot.folder": "Folder", + "feature.screenshot.folder_tooltip": "Relative paths resolve against the Skyrim install dir.\nAbsolute paths (e.g. D:\\Captures) save there directly.", + "feature.screenshot.hdr_bit_depth": "HDR PNG bit depth", + "feature.screenshot.hdr_bit_depth_tooltip": "Quantization for the 48 bpp RGB PNG payload. 11-bit is a good default; higher values increase file size with diminishing returns.", + "feature.screenshot.hdr_note": "HDR enabled: saves the displayed frame as PNG with HDR10 metadata (48 bpp RGB, cICP/cLLi). Use an HDR-aware viewer such as Windows Photos (HDR on) or Special K SKIF.", + "feature.screenshot.hotkey": "Hotkey", + "feature.screenshot.hotkey_collision": "This hotkey collides with vanilla PrintScreen; both saves will fire. Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a different hotkey above.", + "feature.screenshot.name": "Screenshot", + "feature.screenshot.open": "Open", + "feature.screenshot.output": "Output", + "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR and VR captures use the lossless format selected below.", + "feature.screenshot.take_screenshot": "Take Screenshot Now", + "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "A multiplier for the vanilla specular map, applied to the first layer's roughness", + "feature.skin.adds_a_constant_layer_of_wetness_to_all": "Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort.", + "feature.skin.advanced_skin_shader_using_dual_specular_lobes": "Advanced Skin Shader using dual specular lobes.", + "feature.skin.base_color_multiplier": "Base Color Multiplier", + "feature.skin.body_tiling_multiplier": "Body Tiling Multiplier", + "feature.skin.controls_how_bumpy_wet_skin_appears_higher_values": "Controls how bumpy wet skin appears. Higher values create more visible surface ripples and distortion on wet areas.", + "feature.skin.controls_how_much_fine_detail_is_added_to": "Controls how much fine detail is added to the wetness pattern. Higher values add more small-scale variation on top of the base pattern.", + "feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer": "Controls microscopic roughness of stratum corneum layer", + "feature.skin.controls_the_overall_contrast_and_roughness_of_the": "Controls the overall contrast and roughness of the wetness pattern. Higher values make the pattern more pronounced and varied.", + "feature.skin.controls_the_size_of_the_wet_dry_pattern": "Controls the size of the wet/dry pattern on skin. Higher values create a finer, more detailed pattern; lower values produce larger, broader wet patches.", + "feature.skin.description": "Advanced Skin enhances character skin rendering with multiple techniques.", + "feature.skin.dynamic_wetness_detected": "Dynamic Wetness detected.", + "feature.skin.enable_advanced_skin": "Enable Advanced Skin", + "feature.skin.enable_skin_detail": "Enable Skin Detail", + "feature.skin.enable_skin_detail_texture": "Enable skin detail texture", + "feature.skin.enable_sss_transmission": "Enable SSS Transmission", + "feature.skin.extra_edge_roughness": "Extra Edge Roughness", + "feature.skin.extra_roughness_at_the_edges_of_the_skin": "Extra roughness at the edges of the skin, to approximate peach fuzz on the face.", + "feature.skin.extra_skin_wetness": "Extra Skin Wetness", + "feature.skin.fresnel_f0": "Fresnel F0", + "feature.skin.fresnel_reflectance": "Fresnel reflectance", + "feature.skin.full_sweat_threshold": "Full Sweat Threshold", + "feature.skin.fuzz_f0": "Fuzz F0", + "feature.skin.fuzz_roughness": "Fuzz Roughness", + "feature.skin.fuzz_strength": "Fuzz Strength", + "feature.skin.how_many_seconds_it_takes_for_skin_to": "How many seconds it takes for skin to fully dry after leaving water. Higher values mean wetness lingers longer.", + "feature.skin.intensity_of_secondary_specular_highlights": "Intensity of secondary specular highlights", + "feature.skin.key_feature_1": "Physically-based dual specular lobes for realistic skin highlights", + "feature.skin.key_feature_2": "Tiled skin detail textures for enhanced realism", + "feature.skin.key_feature_3": "Extra texture support for roughness, translucency, and wetness", + "feature.skin.key_feature_4": "Reworked wetness system for dynamic skin effects", + "feature.skin.multiplier_for_specular_map": "Multiplier for specular map", + "feature.skin.multiplier_for_the_base_color_texture": "Multiplier for the base color texture", + "feature.skin.multiply_the_tiling_for_the_body_to_match": "Multiply the tiling for the body to match the face", + "feature.skin.name": "Advanced Skin", + "feature.skin.options_for_additional_roughness_and_specular_maps": "Options for additional roughness and specular maps.", + "feature.skin.physical_main_roughness_multiplier": "Physical Main Roughness Multiplier", + "feature.skin.physical_second_roughness_multiplier": "Physical Second Roughness Multiplier", + "feature.skin.physical_specular_multiplier": "Physical Specular Multiplier", + "feature.skin.primary_roughness": "Primary Roughness", + "feature.skin.reload_skin_detail_texture": "Reload Skin Detail Texture", + "feature.skin.secondary_roughness": "Secondary Roughness", + "feature.skin.secondary_specular_strength": "Secondary Specular Strength", + "feature.skin.should_be_30_50_lower_than_primary": "Should be 30-50%% lower than Primary", + "feature.skin.skin_detail_strength": "Skin Detail Strength", + "feature.skin.skin_detail_tiling": "Skin Detail Tiling", + "feature.skin.smoothness_of_epidermal_cell_layer_reflections": "Smoothness of epidermal cell layer reflections", + "feature.skin.specular_texture_multiplier": "Specular Texture Multiplier", + "feature.skin.sss_width": "SSS Width", + "feature.skin.stamina_threshold_for_sweat": "Stamina Threshold for Sweat", + "feature.skin.strength_of_skin_detail_texture": "Strength of skin detail texture", + "feature.skin.the_character_reaches_maximum_sweat_when_stamina_drops": "The character reaches maximum sweat when stamina drops below this percentage. For example, 0.15 means full sweat below 15%% stamina.", + "feature.skin.the_character_starts_sweating_when_their_stamina_drops": "The character starts sweating when their stamina drops below this percentage. For example, 0.75 means sweat appears below 75%% stamina.", + "feature.skin.the_more_tiling_the_more_detailed_the_skin": "The more tiling, the more detailed the skin will be", + "feature.skin.translucency": "Translucency", + "feature.skin.translucency_of_the_sss_transmittance_effect": "Translucency of the SSS Transmittance effect", + "feature.skin.use_dynamic_wetness": "Use Dynamic Wetness", + "feature.skin.wetness_fade_out_time": "Wetness Fade Out Time", + "feature.skin.wetness_normal_scale": "Wetness Normal Scale", + "feature.skin.wetness_perlin_noise_lacunarity": "Wetness Perlin Noise Lacunarity", + "feature.skin.wetness_perlin_noise_persistence": "Wetness Perlin Noise Persistence", + "feature.skin.wetness_perlin_noise_scale": "Wetness Perlin Noise Scale", + "feature.skin.width_of_the_sss_transmittance_effect": "Width of the SSS Transmittance effect", + "feature.sky_sync.custom_angle": "Custom angle", + "feature.sky_sync.custom_angle_tooltip": "Set a custom angle for the sun's trajectory.", + "feature.sky_sync.description": "Synchronizes volumetric lighting and shadows with the actual sun and moon positions in the sky.", + "feature.sky_sync.enabled": "Enabled", + "feature.sky_sync.enabled_tooltip": "Enable or disable Sky Sync features.", + "feature.sky_sync.key_feature_1": "Fixes the mismatch between the positions of the sun and moons and the lighting direction", + "feature.sky_sync.key_feature_2": "Includes a configurable alternative sun path for more realistic and dramatic lighting", + "feature.sky_sync.key_feature_3": "Smoothly switches the light source between the sun and moons based on visibility", + "feature.sky_sync.key_feature_4": "Moon light source can be switched between Masser, Secunda, or the brightest", + "feature.sky_sync.key_feature_5": "Automatic calculation of moon lighting intensity based on moon phase", + "feature.sky_sync.key_feature_6": "Fixes the sun appearing higher on the horizon when the player gains altitude", + "feature.sky_sync.min_shadow_elevation": "Min Shadow Elevation", + "feature.sky_sync.min_shadow_elevation_tooltip": "The minimum angle sunlight will set to. Caps shadow length. Higher = shorter shadows at sunset/sunrise.", + "feature.sky_sync.moon_light_source": "Moon light source", + "feature.sky_sync.moon_light_source_brightest": "Brightest", + "feature.sky_sync.moon_light_source_masser": "Masser", + "feature.sky_sync.moon_light_source_secunda": "Secunda", + "feature.sky_sync.moon_light_source_tooltip": "Select which moon casts shadows during the night.", + "feature.sky_sync.name": "Sky Sync", + "feature.sky_sync.sun_path": "Sun path", + "feature.sky_sync.sun_path_custom": "Custom", + "feature.sky_sync.sun_path_northern": "Northern Sky", + "feature.sky_sync.sun_path_southern": "Southern Sky", + "feature.sky_sync.sun_path_tooltip": "Choose the trajectory the sun takes across the sky.", + "feature.sky_sync.sun_path_vanilla": "Vanilla", + "feature.sky_sync.sun_position_offsets": "Sun Position Offsets", + "feature.sky_sync.sun_position_offsets_desc": "Moves sun height during sunrise/sunset. Reset weather to see changes.", + "feature.sky_sync.sunrise_begin": "Sunrise Begin (Hours)", + "feature.sky_sync.sunrise_begin_tooltip": "Offset for when the sun starts rising.", + "feature.sky_sync.sunrise_end": "Sunrise End (Hours)", + "feature.sky_sync.sunrise_end_tooltip": "Offset for when the sun finishes rising.", + "feature.sky_sync.sunset_begin": "Sunset Begin (Hours)", + "feature.sky_sync.sunset_begin_tooltip": "Offset for when the sun starts setting.", + "feature.sky_sync.sunset_end": "Sunset End (Hours)", + "feature.sky_sync.sunset_end_tooltip": "Offset for when the sun finishes setting.", + "feature.sky_sync.use_alternate_sun_path": "Use alternate sun path", + "feature.sky_sync.use_alternate_sun_path_tooltip": "Calculate sun position based on time of day and season instead of vanilla movement.", + "feature.skylighting.description": "Simulates realistic ambient lighting by calculating sky occlusion and directional lighting, providing more accurate and natural illumination in outdoor environments.", + "feature.skylighting.diffuse_min_visibility": "Diffuse Min Visibility", + "feature.skylighting.key_feature_1": "Sky occlusion calculation for ambient lighting", + "feature.skylighting.key_feature_2": "Directional skylighting based on environment geometry", + "feature.skylighting.key_feature_3": "Enhanced ambient lighting for outdoor scenes", + "feature.skylighting.key_feature_4": "Support for varying sky illumination intensities", + "feature.skylighting.key_feature_5": "Integration with existing lighting systems", + "feature.skylighting.max_zenith": "Max Zenith Angle", + "feature.skylighting.max_zenith_tooltip": "Smaller angles creates more focused top-down shadow.", + "feature.skylighting.min_visibility_desc": "Minimum visibility values. Diffuse darkens objects. Specular removes the sky from reflections.", + "feature.skylighting.name": "Skylighting", + "feature.skylighting.rebuild": "Rebuild Skylighting", + "feature.skylighting.rebuild_tooltip": "Changes below require rebuilding, a loading screen, or moving away from the current location to apply.", + "feature.skylighting.specular_min_visibility": "Specular Min Visibility", + "feature.ssgi.ao_only": "AO only", + "feature.ssgi.ao_power": "AO Power", + "feature.ssgi.ao_radius": "AO radius", + "feature.ssgi.ao_radius_tooltip": "A smaller radius produces tighter AO.", + "feature.ssgi.blur": "Blur", + "feature.ssgi.blur_radius": "Blur Radius", + "feature.ssgi.buffer_viewer": "Buffer Viewer", + "feature.ssgi.debug": "Debug", + "feature.ssgi.denoising": "Denoising", + "feature.ssgi.depth_fade_range": "Depth Fade Range", + "feature.ssgi.depth_fade_range_tooltip": "Distance range where depth-based effects fade out.", + "feature.ssgi.enabled": "Enabled", + "feature.ssgi.enabled_tooltip": "Enable Screen Space Global Illumination. When disabled, all other settings are ignored.", + "feature.ssgi.extreme": "Extreme", + "feature.ssgi.extreme_tooltip": "Full res and clean.", + "feature.ssgi.full_res": "Full Res", + "feature.ssgi.geometry_weight": "Geometry Weight", + "feature.ssgi.geometry_weight_tooltip": "Higher value makes the blur more sensitive to differences in geometry.", + "feature.ssgi.half_res": "Half Res", + "feature.ssgi.hq_specular_il": "(Experimental) HQ Specular IL", + "feature.ssgi.hq_specular_il_tooltip": "An experimental specular GI that is more accurate but requires more samples. Won't be blurred.", + "feature.ssgi.il_distance_compensation": "IL Distance Compensation", + "feature.ssgi.il_distance_compensation_tooltip": "Brighten/Dimming further radiance samples.", + "feature.ssgi.il_radius": "IL radius", + "feature.ssgi.il_radius_tooltip": "A larger radius produces wider IL.", + "feature.ssgi.il_saturation": "IL Saturation", + "feature.ssgi.il_source_brightness": "IL Source Brightness", + "feature.ssgi.indirect_lighting": "Indirect Lighting (IL)", + "feature.ssgi.low": "Low", + "feature.ssgi.low_tooltip": "Quarter res and blurry.", + "feature.ssgi.max_frame_accumulation": "Max Frame Accumulation", + "feature.ssgi.max_frame_accumulation_tooltip": "How many past frames to accumulate results with. Higher values are less noisy but potentially cause ghosting.", + "feature.ssgi.min_screen_radius": "Min Screen Radius", + "feature.ssgi.min_screen_radius_tooltip": "The minimum screen-space effect radius as proportion of display width, to prevent far field AO being too small.", + "feature.ssgi.movement_disocclusion": "Movement Disocclusion", + "feature.ssgi.movement_disocclusion_tooltip": "If a pixel has moved too far from the last frame, its radiance will not be carried to this frame.\nLower values are stricter.", + "feature.ssgi.quality_performance": "Quality/Performance", + "feature.ssgi.quarter_res": "Quarter Res", + "feature.ssgi.reference": "Reference", + "feature.ssgi.reference_tooltip": "Reference mode.", + "feature.ssgi.shader_compile_error": "Compute shaders failed to compile!", + "feature.ssgi.show_advanced": "Show Advanced Options", + "feature.ssgi.slices": "Slices", + "feature.ssgi.slices_tooltip": "How many directions do the samples take.\nControls noise.", + "feature.ssgi.standard": "Standard", + "feature.ssgi.standard_tooltip": "Half res and somewhat stable.", + "feature.ssgi.steps_per_slice": "Steps Per Slice", + "feature.ssgi.steps_per_slice_tooltip": "How many samples does it take in one direction.\nControls accuracy of lighting, and noise when effect radius is large.", + "feature.ssgi.temporal_denoiser": "Temporal Denoiser", + "feature.ssgi.thickness": "Thickness", + "feature.ssgi.thickness_tooltip": "How thick the occluders are. Only affects AO.", + "feature.ssgi.toggles": "Toggles", + "feature.ssgi.vanilla_ssao": "Vanilla SSAO", + "feature.ssgi.vanilla_ssao_tooltip": "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.", + "feature.ssgi.vanilla_ssao_tooltip_vr": "Vanilla SSAO is not supported in VR.", + "feature.ssgi.view_resize": "View Resize", + "feature.ssgi.visual": "Visual", + "feature.ssgi.visual_il": "Visual - IL", + "feature.sss.base_profile": "Base Profile", + "feature.sss.blur_radius": "Blur Radius", + "feature.sss.blur_radius_tooltip": "Blur radius.", + "feature.sss.burley": "Burley", + "feature.sss.burley_samples": "Burley Samples", + "feature.sss.enable_character_lighting": "Enable Character Lighting", + "feature.sss.enable_character_lighting_tooltip": "Vanilla feature, not recommended.", + "feature.sss.falloff": "Falloff", + "feature.sss.human_profile": "Human Profile", + "feature.sss.mean_free_path_color": "Mean Free Path Color", + "feature.sss.mean_free_path_color_tooltip": "Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance.", + "feature.sss.mean_free_path_distance": "Mean Free Path Distance", + "feature.sss.mean_free_path_distance_tooltip": "Controls the distance that Mean Free Path Color goes into subsurface.", + "feature.sss.separable_sss": "Separable SSS", + "feature.sss.settings": "Settings", + "feature.sss.strength": "Strength", + "feature.sss.thickness": "Thickness", + "feature.sss.thickness_tooltip": "Blur radius relative to depth.", + "feature.subsurface_scattering.description": "Subsurface Scattering simulates light penetration through translucent materials like skin, creating more realistic character lighting.\nThis technique makes organic materials appear more lifelike and natural.", + "feature.subsurface_scattering.key_feature_1": "Realistic skin lighting", + "feature.subsurface_scattering.key_feature_2": "Light penetration simulation", + "feature.subsurface_scattering.key_feature_3": "Separate profiles for different materials", + "feature.subsurface_scattering.key_feature_4": "Enhanced character appearance", + "feature.subsurface_scattering.key_feature_5": "Configurable scattering properties", + "feature.subsurface_scattering.name": "Subsurface Scattering", + "feature.terrain_blending.description": "Provides seamless blending between terrain and objects, eliminating harsh transitions where objects meet the ground for more natural-looking landscapes.", + "feature.terrain_blending.enable": "Enable Terrain Blending", + "feature.terrain_blending.enable_tooltip": "Enable seamless blending between terrain and objects.", + "feature.terrain_blending.key_feature_1": "Seamless terrain-to-object blending transitions", + "feature.terrain_blending.key_feature_2": "Advanced depth buffer manipulation for smooth integration", + "feature.terrain_blending.key_feature_3": "Support for alternative terrain rendering modes", + "feature.terrain_blending.key_feature_4": "Multi-pass rendering optimization for complex scenes", + "feature.terrain_blending.key_feature_5": "Enhanced visual continuity in landscape interactions", + "feature.terrain_blending.name": "Terrain Blending", + "feature.terrain_helper.description": "Provides enhanced terrain material support for terrain mods that require additional texture slots and parallax mapping capabilities.", + "feature.terrain_helper.key_feature_1": "Extended texture slot support for terrain materials", + "feature.terrain_helper.key_feature_2": "Parallax mapping integration for terrain textures", + "feature.terrain_helper.key_feature_3": "Automatic terrain material detection and setup", + "feature.terrain_helper.key_feature_4": "Support for advanced terrain modifications", + "feature.terrain_helper.key_feature_5": "Compatibility layer for terrain enhancement mods", + "feature.terrain_helper.name": "Terrain Helper", + "feature.terrain_shadows.buffer_viewer": "Buffer Viewer", + "feature.terrain_shadows.debug": "Debug", + "feature.terrain_shadows.description": "Adds realistic shadow casting from terrain features using heightmap data to create accurate terrain shadows that enhance depth perception and visual realism.", + "feature.terrain_shadows.enable_terrain_shadow": "Enable Terrain Shadow", + "feature.terrain_shadows.key_feature_1": "Heightmap-based terrain shadow calculation", + "feature.terrain_shadows.key_feature_2": "Dynamic shadow updates based on sun position", + "feature.terrain_shadows.key_feature_3": "Support for custom heightmap files", + "feature.terrain_shadows.key_feature_4": "Real-time shadow preprocessing and computation", + "feature.terrain_shadows.key_feature_5": "Integration with existing shadow systems", + "feature.terrain_shadows.name": "Terrain Shadows", + "feature.terrain_variation.apply_to_lod_terrain": "Apply to LOD Terrain", + "feature.terrain_variation.apply_to_lod_terrain_tooltip": "Applies the tiling fix to LOD terrain objects.\nThis helps reduce the visible tiling effect on distant terrain.", + "feature.terrain_variation.description": "Terrain Variation reduces the repeating pattern effect on terrain textures.\nThis technique creates more natural-looking terrain by adding variation to texture sampling.", + "feature.terrain_variation.enable_tiling_fix": "Enable Terrain Tiling Fix", + "feature.terrain_variation.enable_tiling_fix_tooltip": "Reduces the repeating pattern effect on terrain textures.\nThis technique creates more natural-looking terrain by adding variation to texture sampling.", + "feature.terrain_variation.key_feature_1": "Reduces terrain texture tiling", + "feature.terrain_variation.key_feature_2": "Adjustable distance-based blending", + "feature.terrain_variation.key_feature_3": "Improved terrain visual quality", + "feature.terrain_variation.key_feature_4": "Compatible with Extended Materials parallax", + "feature.terrain_variation.name": "Terrain Variation", + "feature.true_pbr.base_color_scale": "Base Color Scale", + "feature.true_pbr.blue": "Blue", + "feature.true_pbr.coat": "Coat", + "feature.true_pbr.coat_color": "Coat Color", + "feature.true_pbr.coat_roughness": "Coat Roughness", + "feature.true_pbr.coat_specular_level": "Coat Specular Level", + "feature.true_pbr.coat_strength": "Coat Strength", + "feature.true_pbr.density_randomization": "Density Randomization", + "feature.true_pbr.displacement_scale": "Displacement Scale", + "feature.true_pbr.enabled": "Enabled", + "feature.true_pbr.glint": "Glint", + "feature.true_pbr.global_settings": "Global Settings", + "feature.true_pbr.green": "Green", + "feature.true_pbr.inner_layer_displacement_offset": "Inner Layer Displacement Offset", + "feature.true_pbr.log_microfacet_density": "Log Microfacet Density", + "feature.true_pbr.material_density_randomization": "Density Randomization", + "feature.true_pbr.material_glint": "Glint", + "feature.true_pbr.material_glint_enabled": "Enabled", + "feature.true_pbr.material_log_microfacet_density": "Log Microfacet Density", + "feature.true_pbr.material_microfacet_roughness": "Microfacet Roughness", + "feature.true_pbr.material_object": "Material Object", + "feature.true_pbr.material_object_settings": "Material Object Settings", + "feature.true_pbr.material_save": "Save", + "feature.true_pbr.material_screenspace_scale": "Screenspace Scale", + "feature.true_pbr.material_specular_level": "Specular Level", + "feature.true_pbr.microfacet_roughness": "Microfacet Roughness", + "feature.true_pbr.name": "True PBR", + "feature.true_pbr.red": "Red", + "feature.true_pbr.reset_to_1_0": "Reset to 1.0", + "feature.true_pbr.roughness": "Roughness", + "feature.true_pbr.roughness_scale": "Roughness Scale", + "feature.true_pbr.save": "Save", + "feature.true_pbr.screenspace_scale": "Screenspace Scale", + "feature.true_pbr.specular_level": "Specular Level", + "feature.true_pbr.subsurface": "Subsurface", + "feature.true_pbr.subsurface_color": "Subsurface Color", + "feature.true_pbr.subsurface_opacity": "Subsurface Opacity", + "feature.true_pbr.texture_set": "Texture Set", + "feature.true_pbr.texture_set_settings": "Texture Set Settings", + "feature.true_pbr.vertex_ao_strength": "Vertex AO Strength", + "feature.unified_water.debug": "Debug", + "feature.unified_water.description": "Unified Water provides a comprehensive fix to water LOD mismatch by replacing distant water tiles with LOD0 (Close Water).", + "feature.unified_water.error_water_cache_generation_failed_for_worldspaces_check": "ERROR: Water cache generation failed for %d WorldSpaces. Check installation and CommunityShaders.log", + "feature.unified_water.generating_water_cache": "Generating Water Cache:", + "feature.unified_water.key_feature_1": "Unifies distant and close water appearance, streamlining all lighting visuals.", + "feature.unified_water.key_feature_2": "Completely and fundamentally resolves water LOD mismatch issues.", + "feature.unified_water.key_feature_3": "Provides background systems for water geometry rendering, allowing more advanced water effects.", + "feature.unified_water.key_feature_4": "Improves vanilla performance by using optimized water meshes for distant water.", + "feature.unified_water.name": "Unified Water", + "feature.unified_water.regenerate_caches": "Regenerate Caches", + "feature.unified_water.regenerate_flowmap": "Regenerate Flowmap", + "feature.unified_water.use_optimised_meshes": "Use Optimised Meshes", + "feature.unified_water.use_optimised_meshes_tooltip": "Uses meshes with significantly lower tri-count for improved performance with no visual quality loss.\nWill only affect newly created water - requires a change of location or game restart to take effect.", + "feature.upscaling.backend_diagnostics": "Backend Diagnostics", + "feature.upscaling.description": "Advanced upscaling and frame generation technologies for improved performance", + "feature.upscaling.dlss_model_preset": "DLSS Model Preset", + "feature.upscaling.dlss_model_preset_default": "Default", + "feature.upscaling.dlss_model_preset_j": "Preset J", + "feature.upscaling.dlss_model_preset_k": "Preset K", + "feature.upscaling.dlss_model_preset_l": "Preset L", + "feature.upscaling.dlss_model_preset_m": "Preset M", + "feature.upscaling.dlss_model_preset_tooltip": "Choose which DLSS AI model preset to use.\nEach model offers different visual quality, performance, and motion stability.\nSet to 'Default' for automatic selection based on your Upscale Preset and hardware.\nChanging this setting requires a restart to take effect.", + "feature.upscaling.force_enable_frame_generation": "Force Enable Frame Generation", + "feature.upscaling.fps_limit": "FPS Limit", + "feature.upscaling.fps_limit_tooltip_1": "Set your frame cap target.", + "feature.upscaling.fps_limit_tooltip_2": "Start about 2-3 FPS below refresh rate (e.g. 117 for 120 Hz).", + "feature.upscaling.frame_generation": "Frame Generation", + "feature.upscaling.frame_generation_available": "AMD FSR Frame Generation is available.", + "feature.upscaling.frame_generation_desc": "Frame Generation interpolates real frames with generated ones for a smoother experience", + "feature.upscaling.frame_generation_in_menus": "Frame Generation in Menus", + "feature.upscaling.frame_generation_in_menus_tooltip_1": "Keeps frame generation active while game menus are open.", + "feature.upscaling.frame_generation_in_menus_tooltip_2": "May feel smoother, but increases menu input latency.", + "feature.upscaling.frame_generation_proxy_note": "Requires a D3D11 to D3D12 proxy which can create compatibility issues", + "feature.upscaling.frame_generation_restart_note": "Toggling this setting requires a restart to work correctly", + "feature.upscaling.frame_generation_tech": "Uses AMD FSR Frame Generation technology", + "feature.upscaling.frame_limit_vrr": "Frame Limit (Variable Refresh Rate)", + "feature.upscaling.key_feature_1": "DLSS (Deep Learning Super Sampling) support", + "feature.upscaling.key_feature_2": "FSR (FidelityFX Super Resolution) support", + "feature.upscaling.key_feature_3": "TAA (Temporal Anti-Aliasing) support", + "feature.upscaling.key_feature_4": "Frame generation for supported systems", + "feature.upscaling.low_latency_boost": "Low Latency Boost", + "feature.upscaling.low_latency_boost_tooltip_1": "Keeps GPU clocks higher to avoid latency spikes at low GPU load.", + "feature.upscaling.low_latency_boost_tooltip_2": "Useful if frametime jumps; costs extra power and heat.", + "feature.upscaling.low_latency_mode": "Low Latency Mode", + "feature.upscaling.low_latency_mode_tooltip_1": "Cuts input delay by syncing CPU work closer to the GPU.", + "feature.upscaling.low_latency_mode_tooltip_2": "Can reduce max FPS a little, but usually feels more responsive.", + "feature.upscaling.marker_optimization_unavailable": "Marker optimization unavailable (PCL not loaded).", + "feature.upscaling.method": "Method", + "feature.upscaling.method_none": "None", + "feature.upscaling.method_taa": "TAA", + "feature.upscaling.name": "Upscaling", + "feature.upscaling.native_inputs": "Native Inputs", + "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", + "feature.upscaling.preset_balanced": "Balanced", + "feature.upscaling.preset_dlaa": "DLAA", + "feature.upscaling.preset_native_aa": "Native AA", + "feature.upscaling.preset_performance": "Performance", + "feature.upscaling.preset_quality": "Quality", + "feature.upscaling.preset_ultra_performance": "Ultra Performance", + "feature.upscaling.reflex_blocked_by_fg": "Reflex is unavailable while the DX12 frame-generation swapchain is active.", + "feature.upscaling.reflex_not_available": "Reflex is not available. Ensure sl.reflex.dll is present and restart.", + "feature.upscaling.sharpness": "Sharpness", + "feature.upscaling.streamline_logging": "Streamline Logging", + "feature.upscaling.streamline_logging_restart_note": "Changing this requires a restart to take effect.", + "feature.upscaling.streamline_logging_tooltip": "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.", + "feature.upscaling.upscale_preset": "Upscale Preset", + "feature.upscaling.upscaling_intermediates": "Upscaling Intermediates", + "feature.upscaling.use_fps_limit": "Use FPS Limit", + "feature.upscaling.use_fps_limit_tooltip_1": "Uses Reflex's internal FPS cap for steadier frametimes.", + "feature.upscaling.use_fps_limit_tooltip_2": "Can lower latency versus uncapped rendering.", + "feature.upscaling.use_markers_to_optimize": "Use Markers To Optimize", + "feature.upscaling.use_markers_to_optimize_tooltip_1": "Uses frame markers for tighter Reflex timing.", + "feature.upscaling.use_markers_to_optimize_tooltip_2": "Try On first; turn Off if it causes stutter on your setup.", + "feature.upscaling.view_resize": "View Resize", + "feature.upscaling.vr_intermediates_not_created": "VR intermediates not yet created (enter game world)", + "feature.volumetric_lighting.description": "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\nThis adds dramatic god rays and atmospheric depth to both interior and exterior environments.", + "feature.volumetric_lighting.enable_exteriors": "Enable Volumetric Lighting in Exteriors", + "feature.volumetric_lighting.enable_interiors": "Enable Volumetric Lighting in Interiors", + "feature.volumetric_lighting.exterior_depth": "Exterior Depth", + "feature.volumetric_lighting.exterior_height": "Exterior Height", + "feature.volumetric_lighting.exterior_quality": "Exterior Quality", + "feature.volumetric_lighting.exterior_width": "Exterior Width", + "feature.volumetric_lighting.interior_depth": "Interior Depth", + "feature.volumetric_lighting.interior_height": "Interior Height", + "feature.volumetric_lighting.interior_quality": "Interior Quality", + "feature.volumetric_lighting.interior_width": "Interior Width", + "feature.volumetric_lighting.key_feature_1": "Realistic light scattering", + "feature.volumetric_lighting.key_feature_2": "God rays and atmospheric effects", + "feature.volumetric_lighting.key_feature_3": "Separate interior/exterior settings", + "feature.volumetric_lighting.key_feature_4": "Configurable quality levels", + "feature.volumetric_lighting.key_feature_5": "Enhanced atmospheric immersion", + "feature.volumetric_lighting.name": "Volumetric Lighting", + "feature.volumetric_lighting.quality_custom": "Custom", + "feature.volumetric_lighting.quality_high": "High", + "feature.volumetric_lighting.quality_low": "Low", + "feature.volumetric_lighting.quality_medium": "Medium", + "feature.volumetric_shadows.description": "Volumetric Shadows provides downsampled VSM shadow maps for use by effects like particles and decals.\nThis improves shadow quality on transparent objects with minimal performance impact.", + "feature.volumetric_shadows.key_feature_1": "Downsampled VSM shadows", + "feature.volumetric_shadows.key_feature_2": "Gaussian blur filtering", + "feature.volumetric_shadows.key_feature_3": "Multi-cascade support", + "feature.volumetric_shadows.key_feature_4": "Optimized for effects rendering", + "feature.volumetric_shadows.name": "Volumetric Shadows", + "feature.vr.description": "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", + "feature.vr.key_feature_1": "Depth buffer culling optimization for VR performance", + "feature.vr.key_feature_2": "In-scene overlay menu with HMD/Controller/Fixed World attach modes", + "feature.vr.key_feature_3": "VR controller input with customizable button mappings", + "feature.vr.key_feature_4": "Grip-to-drag overlay positioning with depth control", + "feature.vr.key_feature_5": "Configurable occlusion culling parameters", + "feature.vr.key_feature_6": "Enhanced VR compatibility with SteamVR and OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "Debug", + "feature.vr_stereo.debug_pom_depth": "Debug POM Depth", + "feature.vr_stereo.disocclusion_depth_threshold": "Disocclusion Depth Threshold", + "feature.vr_stereo.enable": "Enable", + "feature.vr_stereo.enable_stereo_reprojection": "Enable Stereo Reprojection", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.", + "feature.vr_stereo.forward_occlusion_scale": "Forward Occlusion Scale", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.", + "feature.vr_stereo.full_blend_depth_view": "Full Blend Depth View", + "feature.vr_stereo.full_blend_distance": "Full Blend Distance", + "feature.vr_stereo.full_blend_distance_tooltip": "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.", + "feature.vr_stereo.full_blend_zone_hint": " Cyan = full blend zone (closer = stronger tint)", + "feature.vr_stereo.off": "Off", + "feature.vr_stereo.pom_depth_scale": "POM Depth Scale", + "feature.vr_stereo.pom_depth_scale_tooltip": "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.", + "feature.vr_stereo.restart_required": "Restart is required to enable VR stereo reprojection.", + "feature.vr_stereo.skip_pixel_reprojection": "Skip Pixel Reprojection", + "feature.water_effects.description": "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\nThis feature adds dynamic light patterns and improved water visual quality.", + "feature.water_effects.key_feature_1": "Realistic water caustics", + "feature.water_effects.key_feature_2": "Enhanced underwater lighting", + "feature.water_effects.key_feature_3": "Dynamic light patterns on water surfaces", + "feature.water_effects.key_feature_4": "Improved water visual fidelity", + "feature.water_effects.key_feature_5": "Atmospheric underwater effects", + "feature.water_effects.name": "Water Effects", + "feature.wetness_effects.advanced": "Advanced", + "feature.wetness_effects.breadth": "Breadth", + "feature.wetness_effects.chance": "Chance", + "feature.wetness_effects.chance_tooltip": "Portion of raindrops that will actually cause splashes and ripples. Higher values increase effect density but have the least performance impact.", + "feature.wetness_effects.climate_arctic_detail_0": "Cold, dry climate with minimal precipitation.", + "feature.wetness_effects.climate_arctic_detail_1": "Max precipitation: ~1.08 mm/hr (light)", + "feature.wetness_effects.climate_arctic_detail_2": "Multipliers: Wetness 0.5x, Puddle 0.3x, Transition 0.5x.", + "feature.wetness_effects.climate_arctic_detail_3": "Raindrop: 30% chance, grid 3.5 units, interval 0.4s.", + "feature.wetness_effects.climate_arctic_detail_4": "Performance impact: Minimal", + "feature.wetness_effects.climate_arctic_effect_0": "Slow wetness accumulation (0.5x)", + "feature.wetness_effects.climate_arctic_effect_1": "Minimal puddle formation (0.3x)", + "feature.wetness_effects.climate_arctic_effect_2": "Slow weather transitions (0.5x)", + "feature.wetness_effects.climate_arctic_effect_3": "Sparse precipitation (30% chance)", + "feature.wetness_effects.climate_coastal_detail_0": "Maritime climate with frequent, heavy precipitation.", + "feature.wetness_effects.climate_coastal_detail_1": "Max precipitation: ~8.06 mm/hr (heavy)", + "feature.wetness_effects.climate_coastal_detail_2": "Multipliers: Wetness 1.5x, Puddle 1.7x, Transition 1.7x.", + "feature.wetness_effects.climate_coastal_detail_3": "Raindrop: 80% chance, grid 2.5 units, interval 0.25s.", + "feature.wetness_effects.climate_coastal_detail_4": "Performance impact: Moderate", + "feature.wetness_effects.climate_coastal_effect_0": "Fast wetness accumulation (1.5x)", + "feature.wetness_effects.climate_coastal_effect_1": "Enhanced puddle formation (1.7x)", + "feature.wetness_effects.climate_coastal_effect_2": "Rapid weather transitions (1.7x)", + "feature.wetness_effects.climate_coastal_effect_3": "Frequent rain events (80% chance)", + "feature.wetness_effects.climate_legacy_detail_0": "Riverwood's original rain effect values for full backward compatibility.", + "feature.wetness_effects.climate_legacy_detail_1": "Max precipitation: ~0.66 mm/hr (very light)", + "feature.wetness_effects.climate_legacy_detail_2": "Multipliers: Wetness 1.0x, Puddle 1.0x, Transition 1.0x.", + "feature.wetness_effects.climate_legacy_detail_3": "Raindrop: 30% chance, grid 4.0 units, interval 0.5s.", + "feature.wetness_effects.climate_legacy_detail_4": "Performance impact: Minimal (baseline)", + "feature.wetness_effects.climate_legacy_effect_0": "Original wetness accumulation (1.0x)", + "feature.wetness_effects.climate_legacy_effect_1": "Original puddle formation (1.0x)", + "feature.wetness_effects.climate_legacy_effect_2": "Original weather transitions (1.0x)", + "feature.wetness_effects.climate_legacy_effect_3": "Original raindrop frequency (1.0x)", + "feature.wetness_effects.climate_monsoon_detail_0": "Tropical/monsoon climate with extreme precipitation.", + "feature.wetness_effects.climate_monsoon_detail_1": "Max precipitation: ~22 mm/hr (extreme)", + "feature.wetness_effects.climate_monsoon_detail_2": "Multipliers: Wetness 2.0x, Puddle 2.5x, Transition 2.0x.", + "feature.wetness_effects.climate_monsoon_detail_3": "Raindrop: 100% chance, grid 2.0 units, interval 0.2s.", + "feature.wetness_effects.climate_monsoon_detail_4": "Skyrim light rain will not match wetness.", + "feature.wetness_effects.climate_monsoon_detail_5": "Performance impact: High (may impact GPU)", + "feature.wetness_effects.climate_monsoon_effect_0": "Rapid wetness accumulation (2.0x)", + "feature.wetness_effects.climate_monsoon_effect_1": "Maximum puddle formation (2.5x)", + "feature.wetness_effects.climate_monsoon_effect_2": "Very dynamic weather (2.0x)", + "feature.wetness_effects.climate_monsoon_effect_3": "Maximum raindrop frequency (100% chance)", + "feature.wetness_effects.climate_nordic_detail_0": "Balanced temperate Nordic climate.", + "feature.wetness_effects.climate_nordic_detail_1": "Max precipitation: ~3.35 mm/hr (moderate)", + "feature.wetness_effects.climate_nordic_detail_2": "Multipliers: Wetness 1.0x, Puddle 1.0x, Transition 1.0x.", + "feature.wetness_effects.climate_nordic_detail_3": "Raindrop: 100% chance, grid 3.0 units, interval 1.0s.", + "feature.wetness_effects.climate_nordic_detail_4": "Performance impact: Low", + "feature.wetness_effects.climate_nordic_effect_0": "Standard wetness accumulation (1.0x)", + "feature.wetness_effects.climate_nordic_effect_1": "Standard puddle formation (1.0x)", + "feature.wetness_effects.climate_nordic_effect_2": "Standard weather transitions (1.0x)", + "feature.wetness_effects.climate_nordic_effect_3": "Moderate raindrop frequency (100% chance)", + "feature.wetness_effects.climate_preset": "Climate Preset", + "feature.wetness_effects.climate_preset_arctic": "Arctic Tundra", + "feature.wetness_effects.climate_preset_arctic_desc": "Cold, dry Arctic climate (light rain)", + "feature.wetness_effects.climate_preset_coastal": "Temperate Coastal", + "feature.wetness_effects.climate_preset_coastal_desc": "Maritime climate (heavy rain)", + "feature.wetness_effects.climate_preset_custom": "Custom", + "feature.wetness_effects.climate_preset_custom_desc": "User-defined custom settings", + "feature.wetness_effects.climate_preset_legacy": "Legacy", + "feature.wetness_effects.climate_preset_legacy_desc": "Original rain effect values (very light)", + "feature.wetness_effects.climate_preset_monsoon": "Monsoon/Extreme", + "feature.wetness_effects.climate_preset_monsoon_desc": "Extreme monsoon climate (extreme rain)", + "feature.wetness_effects.climate_preset_nordic": "Nordic (Default)", + "feature.wetness_effects.climate_preset_nordic_desc": "Balanced Nordic climate (moderate rain)", + "feature.wetness_effects.climate_preset_unknown": "Unknown", + "feature.wetness_effects.climate_presets": "Climate Presets", + "feature.wetness_effects.custom_preset_tooltip_0": "Custom settings - you have modified the preset values.", + "feature.wetness_effects.custom_preset_tooltip_1": "Select a preset above to apply predefined climate settings.", + "feature.wetness_effects.debug": "Debug", + "feature.wetness_effects.description": "Adds realistic wetness effects including rain-based surface wetness, puddle formation, shore wetness, and dynamic raindrop effects for enhanced weather immersion.", + "feature.wetness_effects.effect_range": "Effect Range", + "feature.wetness_effects.effect_range_tooltip": "Range for raindrop effects", + "feature.wetness_effects.effects": "Effects:", + "feature.wetness_effects.enable_interior_exterior_override": "Enable Interior/Exterior Override", + "feature.wetness_effects.enable_puddle_override": "Enable Puddle Override", + "feature.wetness_effects.enable_rain_override": "Enable Rain Override", + "feature.wetness_effects.enable_raindrop_effects": "Enable Raindrop Effects", + "feature.wetness_effects.enable_ripples": "Enable Ripples", + "feature.wetness_effects.enable_ripples_tooltip": "Enables circular ripples on puddles, and to a less extent other wet surfaces", + "feature.wetness_effects.enable_splashes": "Enable Splashes", + "feature.wetness_effects.enable_splashes_tooltip": "Enables small splashes of wetness on dry surfaces.", + "feature.wetness_effects.enable_vanilla_ripples": "Enable Vanilla Ripples", + "feature.wetness_effects.enable_vanilla_ripples_controlled": "Enable Vanilla Ripples - Controlled by Splashes of Storms", + "feature.wetness_effects.enable_wetness": "Enable Wetness", + "feature.wetness_effects.enable_wetness_override": "Enable Wetness Override", + "feature.wetness_effects.enable_wetness_tooltip": "Enables a wetness effect near water and when it is raining.", + "feature.wetness_effects.grid_size": "Grid Size", + "feature.wetness_effects.grid_size_tooltip_0": "Spatial grid size for raindrop placement (smaller = more grid cells, higher GPU cost)", + "feature.wetness_effects.grid_size_tooltip_1": "This is the most performance-sensitive setting. Lower only if needed for realism.", + "feature.wetness_effects.interior_exterior_override_tooltip": "If disabled, will only use the exterior value. ", + "feature.wetness_effects.interval": "Interval", + "feature.wetness_effects.interval_tooltip": "How often raindrop effects are checked (lower = more frequent, moderate performance impact)", + "feature.wetness_effects.key_feature_1": "Dynamic surface wetness based on weather conditions", + "feature.wetness_effects.key_feature_2": "Realistic puddle formation and shore wetness effects", + "feature.wetness_effects.key_feature_3": "Animated raindrop effects with splashes and ripples", + "feature.wetness_effects.key_feature_4": "Configurable wetness intensity and weather transitions", + "feature.wetness_effects.key_feature_5": "Support for skin wetness and material-specific responses", + "feature.wetness_effects.lifetime": "Lifetime", + "feature.wetness_effects.max_radius": "Max Radius", + "feature.wetness_effects.meters_format": "{:.2f} meters", + "feature.wetness_effects.min_radius": "Min Radius", + "feature.wetness_effects.min_rain_wetness": "Min Rain Wetness", + "feature.wetness_effects.min_rain_wetness_tooltip": "The minimum amount an object gets wet from rain.", + "feature.wetness_effects.name": "Wetness Effects", + "feature.wetness_effects.open_feature": "Open {}", + "feature.wetness_effects.open_installed_feature_tooltip": "Open the installed %s feature", + "feature.wetness_effects.portion_of_grid_size": "As portion of grid size.", + "feature.wetness_effects.puddle_max_angle": "Puddle Max Angle", + "feature.wetness_effects.puddle_max_angle_tooltip": "How flat a surface needs to be for puddles to form on it.", + "feature.wetness_effects.puddle_min_wetness": "Puddle Min Wetness", + "feature.wetness_effects.puddle_min_wetness_tooltip": "The wetness value at which puddles start to form.", + "feature.wetness_effects.puddle_radius": "Puddle Radius", + "feature.wetness_effects.puddle_radius_tooltip": "The radius used to determine puddle size and location", + "feature.wetness_effects.puddle_wetness": "Puddle Wetness", + "feature.wetness_effects.puddle_wetness_in_exterior": "Puddle Wetness In/Exterior", + "feature.wetness_effects.radius": "Radius", + "feature.wetness_effects.rain_in_exterior": "Rain In/Exterior", + "feature.wetness_effects.rain_wetness": "Rain Wetness", + "feature.wetness_effects.raindrop_effects": "Raindrop Effects", + "feature.wetness_effects.raindrops": "Raindrops", + "feature.wetness_effects.raindrops_help": "At every interval, a raindrop is placed within each grid cell.\nOnly a set portion of raindrops will actually trigger splashes and ripples.\n", + "feature.wetness_effects.ripples": "Ripples", + "feature.wetness_effects.shore_range": "Shore Range", + "feature.wetness_effects.shore_range_tooltip": "The maximum distance from a body of water that Shore Wetness affects", + "feature.wetness_effects.shore_wetness": "Shore Wetness", + "feature.wetness_effects.skin_wetness": "Skin Wetness", + "feature.wetness_effects.skin_wetness_tooltip": "How wet character skin and hair get during rain.", + "feature.wetness_effects.splashes": "Splashes", + "feature.wetness_effects.strength": "Strength", + "feature.wetness_effects.vanilla_ripples_tooltip_0": "Enables default ripples (e.g., Ripples01).", + "feature.wetness_effects.vanilla_ripples_tooltip_1": "Disabling may not take effect until the next weather change.", + "feature.wetness_effects.weather_transition_speed": "Weather transition speed", + "feature.wetness_effects.weather_transition_speed_tooltip": "How fast wetness appears when raining and how quickly it dries after rain has stopped.", + "feature.wetness_effects.wetness_effects": "Wetness Effects", + "feature.wetness_effects.wetness_in_exterior": "Wetness In/Exterior", + "menu.advanced.active_shaders": "Active Shaders", + "menu.advanced.active_shaders_tooltip": "List of shaders that have been used in recent frames. Enable Shader Blocking above to use hotkeys to cycle through and block shaders for debugging. Shaders not used for ~1 second are removed from this list.", + "menu.advanced.active_shaders_used_recently": "Active Shaders (Used Recently)", + "menu.advanced.addresses": "Addresses", + "menu.advanced.avg_parallelism_metric": "Average parallelism (W/S): %.2fx", + "menu.advanced.avg_parallelism_tooltip_1": "Average useful concurrency in this workload.", + "menu.advanced.avg_parallelism_tooltip_2": "Roughly the worker count where adding more cores gives diminishing returns.", + "menu.advanced.avoid_flow_control": "Avoid Flow Control", + "menu.advanced.avoid_flow_control_tooltip": "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\nForces fxc to flatten branches into predicated ops rather than emitting dynamic flow control. Often a win for short branch bodies and uniformly-taken branches; usually a loss for long divergent branches that vanilla flow control would skip entirely.\nResets every launch. Toggling this clears the shader cache and triggers a full recompile.", + "menu.advanced.background_compiler_threads": "Background Compiler Threads", + "menu.advanced.background_compiler_threads_tooltip": "Number of threads used to compile shaders during gameplay. Defaults to half of performance cores to avoid impacting the render thread. Higher values finish compilation faster but may cause stuttering.", + "menu.advanced.block_next": "Block Next:", + "menu.advanced.block_previous": "Block Previous:", + "menu.advanced.blocked_shader": "Blocked: %s", + "menu.advanced.change_shader_block_next": "Change##ShaderBlockNext", + "menu.advanced.change_shader_block_prev": "Change##ShaderBlockPrev", + "menu.advanced.clear_shader_cache": "Clear Shader Cache", + "menu.advanced.clear_shader_cache_tooltip": "Clear all compiled shaders from memory. Forces recompilation of all shaders on next use.", + "menu.advanced.click_to_block": "Left-click to block this shader", + "menu.advanced.click_to_unblock": "Left-click to unblock this shader", + "menu.advanced.column_class": "Class", + "menu.advanced.column_class_tooltip": "Shader class", + "menu.advanced.column_descriptor": "Descriptor", + "menu.advanced.column_descriptor_tooltip": "Shader descriptor", + "menu.advanced.column_frame_pct": "Frame %", + "menu.advanced.column_frame_pct_tooltip": "Percentage of draw calls this frame", + "menu.advanced.column_key": "Key", + "menu.advanced.column_key_tooltip": "Shader key", + "menu.advanced.column_type": "Type", + "menu.advanced.column_type_tooltip": "Shader type", + "menu.advanced.compiler_threads": "Compiler Threads", + "menu.advanced.compiler_threads_tooltip": "Number of threads used to compile shaders at startup. Defaults to all logical cores minus one for OS headroom (E-cores included). Higher values finish compilation faster but may make the system less responsive.", + "menu.advanced.compute": "Compute", + "menu.advanced.compute_tooltip": "Replace Compute Shaders. When false, will disable the custom Compute Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", + "menu.advanced.copy_info": "Copy Info", + "menu.advanced.copy_info_tooltip": "Copy complete shader information including cache path to clipboard", + "menu.advanced.copy_key": "Copy key", + "menu.advanced.dump_ini_settings": "Dump Ini Settings", + "menu.advanced.dump_shaders": "Dump Shaders", + "menu.advanced.dump_shaders_tooltip": "Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this.", + "menu.advanced.efficiency_progress": "{:.1f}% efficient / {:.1f}% gap", + "menu.advanced.enable_file_watcher": "Enable File Watcher", + "menu.advanced.enable_file_watcher_tooltip": "Automatically recompile shaders on file change. Intended for developing.", + "menu.advanced.enable_shader_blocking": "Enable Shader Blocking", + "menu.advanced.enable_shader_blocking_tooltip": "Enables hotkeys to cycle through and block individual shaders for debugging purposes.", + "menu.advanced.frame_annotations": "Frame Annotations", + "menu.advanced.frame_annotations_tooltip": "Enable detailed frame annotations for debugging render passes and draw calls.", + "menu.advanced.half_precision": "Half Precision (Partial Precision)", + "menu.advanced.half_precision_tooltip": "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\nLets fxc downgrade unmarked float ops to FP16 where it can prove safety, on top of the existing min16float type hints.\nOn FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register pressure and double ALU throughput, but it can also introduce minor visual differences in shaders that haven't been audited for precision sensitivity.\nToggling this clears the shader cache and triggers a full recompile.", + "menu.advanced.infinite_core_efficiency": "Infinite-core efficiency", + "menu.advanced.infinite_core_efficiency_metric": "Infinite-core efficiency (S/T_p): %.1f%%", + "menu.advanced.infinite_core_efficiency_tooltip_1": "How close runtime is to the infinite-core lower bound.", + "menu.advanced.infinite_core_efficiency_tooltip_2": "100%% means T_p == S.", + "menu.advanced.infinite_core_gap_metric": "Infinite-core gap: %.1f%%", + "menu.advanced.infinite_core_gap_tooltip_1": "Distance from ideal infinite-core time.", + "menu.advanced.infinite_core_gap_tooltip_2": "Defined as 100 * (1 - S / T_p). Lower is better.", + "menu.advanced.log_level": "Log Level", + "menu.advanced.log_level_critical": "critical", + "menu.advanced.log_level_debug": "debug", + "menu.advanced.log_level_err": "err", + "menu.advanced.log_level_info": "info", + "menu.advanced.log_level_off": "off", + "menu.advanced.log_level_tooltip": "Log level. Trace is most verbose. Default is info.", + "menu.advanced.log_level_trace": "trace", + "menu.advanced.log_level_warn": "warn", + "menu.advanced.makespan_label": "Makespan (T_p)", + "menu.advanced.makespan_metric": "Makespan (T_p): %s", + "menu.advanced.makespan_tooltip": "Observed wall-clock duration for the full shader build.", + "menu.advanced.open_logs": "Open Logs", + "menu.advanced.parallelism_header": "Parallelism (derived from %zu compiled tasks)", + "menu.advanced.parallelism_tooltip_1": "Computed lazily from the last completed build.", + "menu.advanced.parallelism_tooltip_2": "Only evaluated when this Statistics section is open.", + "menu.advanced.pixel": "Pixel", + "menu.advanced.pixel_tooltip": "Replace Pixel Shaders. When false, will disable the custom Pixel Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", + "menu.advanced.press_key_shader_block_next": "Press any key for Shader Block Next...", + "menu.advanced.press_key_shader_block_prev": "Press any key for Shader Block Previous...", + "menu.advanced.queue_wait_metric": "Queue wait (avg/max): %s / %s", + "menu.advanced.queue_wait_tooltip_1": "Time spent waiting in the ready queue before a worker started compilation.", + "menu.advanced.queue_wait_tooltip_2": "Useful for identifying scheduler-induced delay separate from compile cost.", + "menu.advanced.relative_bar_format": "{} ({:.1f}%)", + "menu.advanced.relative_durations": "Relative durations (normalized)", + "menu.advanced.replace_original_shaders": "Replace Original Shaders", + "menu.advanced.shader_blocking_active": "Shader Blocking Active", + "menu.advanced.shader_class_label": "Class: %s", + "menu.advanced.shader_compiler_stats": "Shader Compiler : {}", + "menu.advanced.shader_debug_header": "Shader Debug", + "menu.advanced.shader_defines": "Shader Defines", + "menu.advanced.shader_defines_tooltip": "Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile.", + "menu.advanced.shader_descriptor": "Descriptor: 0x%X", + "menu.advanced.shader_row_tooltip": "Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\n\n{}", + "menu.advanced.shader_slow_entry": "#%zu %s (weight %d)", + "menu.advanced.shader_type_label": "Type: %s", + "menu.advanced.span_label": "Span (S)", + "menu.advanced.span_metric": "Span (S, longest): %s", + "menu.advanced.span_tooltip_1": "Critical-path lower bound, approximated by the single slowest shader.", + "menu.advanced.span_tooltip_2": "Even infinite cores cannot finish faster than this.", + "menu.advanced.statistics": "Statistics", + "menu.advanced.stop_blocking": "Stop Blocking##Section", + "menu.advanced.tab_developer": "Developer", + "menu.advanced.tab_disable_at_boot": "Disable at Boot", + "menu.advanced.tab_logging": "Logging", + "menu.advanced.tab_shader_debug": "Shader Debug", + "menu.advanced.tab_testing": "Testing", + "menu.advanced.test_conditions": "Test Conditions", + "menu.advanced.top_slowest_shaders": "Top %zu Slowest Shaders (last build)", + "menu.advanced.vertex": "Vertex", + "menu.advanced.vertex_tooltip": "Replace Vertex Shaders. When false, will disable the custom Vertex Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", + "menu.advanced.work_label": "Work (W)", + "menu.advanced.work_metric": "Work (W, sum of task wall times): %s", + "menu.advanced.work_tooltip_1": "Total compile work: sum of all per-shader wall-clock compile times.", + "menu.advanced.work_tooltip_2": "This is not CPU time; it is accumulated task elapsed time.", + "menu.advanced.work_tooltip_3": "Equivalent serial time on one worker if overhead stayed the same.", + "menu.clear_shader_cache": "Clear Shader Cache", + "menu.clear_shader_cache_tooltip": "Clears the shader cache and disk cache (if enabled). The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. The Disk Cache is a collection of compiled shaders on disk. Clearing will mean that shaders are recompiled only when the game re-encounters them.", + "menu.disable_at_boot_desc": "Select features to disable at boot. This is the same as deleting a feature.ini file. Restart will be required to reenable.", + "menu.faq.a1": "Community Shaders is a comprehensive graphics enhancement framework for Skyrim that provides advanced lighting, materials, and visual effects. It's designed to be modular, allowing you to enable only the features you want while maintaining good performance.", + "menu.faq.a2": "Each feature can be found in the left sidebar menu. Click on any feature to access its settings. Most features include presets and detailed tooltips to help you understand what each setting does.", + "menu.faq.a3": "Features may fail to load due to hardware incompatibility, missing dependencies, or conflicts with other mods. Check the 'Feature Issues' tab for detailed information about any problematic features.", + "menu.faq.a4": "Failed shaders are usually caused by mixed file versions. Ensure all features are up to date and avoid mixing files from test builds or outdated versions. Please review the 'Feature Issues' tab and/or Wiki for more information. Update your features and remove any obsolete features.", + "menu.faq.a5": "Start by enabling the Performance Overlay to monitor your FPS. Consider disabling expensive features like Screen Space GI or reducing quality settings. The 'Display' tab also includes upscaling options that can improve performance.", + "menu.faq.a6": "No, Community Shaders is not compatible with ENB. Community Shaders will automatically disable itself if ENB is detected.", + "menu.faq.a7": "By default, Community Shaders uses the END key to open this menu. If your keyboard doesn't have an END key or it's not working, you can change it in the General > Keybindings tab. You can also edit the hotkey in the JSON configuration files.", + "menu.faq.a8": "We're always looking for talented developers to join the team! Check out our GitHub wiki for contribution guidelines and join our Discord server to connect with the development team. Whether you're interested in shader programming, C++ development, or documentation, there's always something to contribute.", + "menu.faq.a9": "Yes! Community Shaders is completely open source and available on GitHub. You can view the source code, report issues, suggest features, and contribute to the project. The project is licensed under GPL, ensuring it remains free and open for everyone. Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence. Any included assets may not be used without explicit permission.", + "menu.faq.q1": "What is Community Shaders?", + "menu.faq.q2": "How do I configure features?", + "menu.faq.q3": "Why are some features not loading?", + "menu.faq.q4": "I have \"Failed Shaders\" when compiling?", + "menu.faq.q5": "How do I improve performance?", + "menu.faq.q6": "Is Community Shaders compatible with ENB?", + "menu.faq.q7": "The menu hotkey isn't working!", + "menu.faq.q8": "I would like to help develop Community Shaders.", + "menu.faq.q9": "Is Community Shaders open source?", + "menu.faq.title": "Frequently Asked Questions", + "menu.features": "Features", + "menu.features.advanced": "Advanced", + "menu.features.also_feature": "Also: %s", + "menu.features.apply_override": "Apply Override", + "menu.features.available_after_restart": "This feature will be available after restart.", + "menu.features.boot_toggle_tooltip": "Toggle feature loading at boot.\nCurrent state: %s\nRestart required for changes to take effect.\nDisabling removes performance impact.", + "menu.features.cannot_apply_overrides_scene": "Cannot apply overrides while scene-specific settings are active.\nPause scene settings for this feature first.", + "menu.features.click_to_navigate": "Click to navigate to %s", + "menu.features.col_constrained_by": "Constrained By", + "menu.features.col_forced_to": "Forced To", + "menu.features.col_impacted_feature": "Impacted Feature", + "menu.features.col_setting": "Setting", + "menu.features.constraints_explanation": "These settings are disabled in their respective feature menus while the constraints are active. Adjust the constraining features to remove them.", + "menu.features.disabled": "Disabled", + "menu.features.display": "Display", + "menu.features.dont_show_warning": "Don't show this warning again", + "menu.features.download_link": "Click here to download this feature ({})", + "menu.features.download_tooltip": "Download the feature from the mod page.", + "menu.features.enable_to_access_config": "Enable the feature above to access its configuration options.", + "menu.features.enabled": "Enabled", + "menu.features.error_header": "Error", + "menu.features.feature_issues": "Feature Issues", + "menu.features.features": "Features", + "menu.features.general": "General", + "menu.features.home": "Home", + "menu.features.no_settings_available": "There are no settings available for this feature.", + "menu.features.ok_button": "OK", + "menu.features.pause_weather_overrides": "Pause Weather Overrides", + "menu.features.pause_weather_tooltip": "Temporarily disable weather-based setting adjustments for this feature.\nThis state is not saved.", + "menu.features.profiling": "Profiling", + "menu.features.restore_defaults_tooltip": "Restore default settings for this feature", + "menu.features.restore_override_tooltip": "Restores original override settings from mod files.\nThis will discard your customizations and revert to\nthe mod author's recommended settings.", + "menu.features.scene_specific_settings": "Scene Specific Settings", + "menu.features.select_feature_left": "Please select a feature from the left.", + "menu.features.select_item_left": "Please select an item on the left.", + "menu.features.settings_adjusted_warning": "Some of your settings have been automatically adjusted due to feature incompatibilities.", + "menu.features.settings_hidden_disabled": "Feature settings are hidden because this feature is disabled at boot.", + "menu.features.unloaded_features": "Unloaded Features", + "menu.footer.d3d12_swap_chain": "D3D12 Swap Chain: {status}", + "menu.footer.game_version": "Game Version: {runtime} {version}", + "menu.footer.gpu": "GPU: {name}", + "menu.home.active_constraints": "Active Setting Constraints", + "menu.home.click_to_navigate": "Click to navigate to {feature}", + "menu.home.consider_disabling_at_boot": "Consider disabling at boot.", + "menu.home.constraint_header_constrained_by": "Constrained By", + "menu.home.constraint_header_forced_to": "Forced To", + "menu.home.constraint_header_setting": "Setting", + "menu.home.constraints_desc": "Some settings are constrained by other features. Hover over rows for details.", + "menu.home.dev_wiki": "Developer Wiki", + "menu.home.github": "GitHub", + "menu.home.intro": "Community Shaders provides advanced graphics enhancements for Skyrim.\nThis comprehensive collection of features brings modern rendering techniques\nto enhance your visual experience.", + "menu.home.join_discord": "Join our Discord", + "menu.home.nexus_mods": "Nexus Mods", + "menu.home.quick_links": "Quick Links", + "menu.home.welcome": "Welcome to Community Shaders {version}", + "menu.home.welcome_dev": "Welcome to Community Shaders {version} [{build}]", + "menu.home.wiki": "Wiki", + "menu.issues.all_ini_loading": "All feature INI files are loading successfully.", + "menu.issues.cancel": "Cancel", + "menu.issues.cannot_be_undone": "This action cannot be undone!", + "menu.issues.check_modified_files": "Check for modified files in Data/Shaders/ (not in feature subfolders)", + "menu.issues.cleanup_actions": "Cleanup Actions:", + "menu.issues.clear_issue_list": "Clear Issue List", + "menu.issues.clear_issue_list_tooltip": "Clears this issue list (useful after cleanup).", + "menu.issues.compilation_breaking_desc": "The following features modified core shader files and must be completely uninstalled via your mod manager. Deleting just the INI file will not fix compilation errors if core shaders were modified.", + "menu.issues.compilation_breaking_header": "Compilation Breaking Features", + "menu.issues.compilation_persist_warning": "If compilation issues persist after deletion:", + "menu.issues.core_feature_installed": "Core feature already installed", + "menu.issues.core_feature_installed_tooltip": "This feature is already included as part of the core Community Shaders installation. Uninstall this feature with your mod manager.", + "menu.issues.current_version": "Current Version: %s", + "menu.issues.delete": "Delete", + "menu.issues.delete_confirm": "Are you sure? This will delete all files for feature '%s'?", + "menu.issues.delete_files_tooltip": "Delete all files associated with this feature (INI, shaders, etc.)", + "menu.issues.delete_unknown_tooltip": "Delete files for this unknown feature. WARNING: If this feature modified core shaders, deletion may not fix compilation issues.", + "menu.issues.download_tooltip": "Download {name}", + "menu.issues.download_version_tooltip": "Download {name} version {version} or later", + "menu.issues.file_label": "File: %s", + "menu.issues.files_label": "Files:", + "menu.issues.general_actions": "General Actions:", + "menu.issues.guidance_label": "Guidance: %s", + "menu.issues.hlsl_files_count": "%zu HLSL files", + "menu.issues.hlsl_files_found": "HLSL Files: %zu found", + "menu.issues.ini_file_label": "INI file: %s", + "menu.issues.ini_label": "INI: %s", + "menu.issues.ini_path": "INI Path: %s", + "menu.issues.issue_label": "Issue: %s", + "menu.issues.last_modified": "Last Modified:", + "menu.issues.minimum_required": "Minimum Required: %s", + "menu.issues.no_issues": "No feature issues found!", + "menu.issues.obsolete_compilation_failure": "This obsolete feature modified core shader files and is causing compilation failures. It must be uninstalled via mod manager.", + "menu.issues.obsolete_features_desc": "The following features are obsolete and disabled automatically. These features have been removed or replaced in this CS version but do not modify core shaders.", + "menu.issues.obsolete_features_header": "Obsolete Features", + "menu.issues.open_features_folder": "Open Features Folder", + "menu.issues.open_features_folder_tooltip": "Opens the Features folder containing INI files for manual review.", + "menu.issues.open_logs": "Open Logs", + "menu.issues.open_logs_tooltip": "Opens the CommunityShaders.log file for manual review.", + "menu.issues.open_shaders_directory": "Open Shaders Directory", + "menu.issues.open_shaders_tooltip": "Opens the main Shaders directory to view individual feature shader folders.", + "menu.issues.override_failures_desc": "The following override files failed to load or apply. Check the file format and content.", + "menu.issues.override_failures_header": "Override Failures", + "menu.issues.potential_compilation_failure": "POTENTIAL COMPILATION FAILURE", + "menu.issues.reinstall_cs": "Consider reinstalling Community Shaders if issues persist", + "menu.issues.replaced_by_prefix": "(replaced by ", + "menu.issues.replaced_by_suffix": ")", + "menu.issues.replacement_label": "Replacement: %s", + "menu.issues.shader_directory_label": "Shader directory: %s", + "menu.issues.shader_folder": "Shader Folder: %s", + "menu.issues.test.active_inis_count": "Active test INI files ({count}):\n", + "menu.issues.test.active_inis_warning": "Test INI files are currently active. Restart CS to see feature issues.", + "menu.issues.test.create_test_inis": "Create Test Inis", + "menu.issues.test.create_test_inis_tooltip": "Creates test INI files that trigger all known feature issue cases:\n- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n- Unknown features (fake non-existent features)\n- Version mismatch (modifies existing feature version)\nRestart CS after creating to see the issues in action.", + "menu.issues.test.feature_issue_testing": "Feature Issue Testing", + "menu.issues.test.feature_issue_testing_desc": "These tools create test INI files to trigger all known feature issue types for testing purposes.", + "menu.issues.test.modified_notice": "\nSome test files modified - restore recommended to clean up", + "menu.issues.test.no_active_inis": "No test INI files are currently active.", + "menu.issues.test.restore": "Restore", + "menu.issues.test.restore_tooltip": "Removes all test INI files and restores any modified INI files to their original state.\nThis undoes all changes made by 'Create Test Inis'.\nRestart CS after restoring to see normal operation.", + "menu.issues.test.testing_header": "Testing", + "menu.issues.this_will_delete": "This will delete:", + "menu.issues.time_label": "Time: %s", + "menu.issues.uninstall_via_mod_manager": "Completely uninstall the feature via your mod manager", + "menu.issues.unknown_compilation_warning": "This unknown feature may have modified core shader files and could be causing compilation failures. Unknown features should be removed if failures continue.", + "menu.issues.unknown_delete_warning": "This is an UNKNOWN feature. If it modified core shader files (outside of its own folder), deleting these files alone will NOT fix shader compilation issues.", + "menu.issues.unknown_features_desc": "The following features are not recognized and we tried to disable automatically. They may be from development branches or newer CS versions. Since we cannot determine what files they may have modified, they should be removed as a precaution to prevent potential shader compilation failures.", + "menu.issues.unknown_features_header": "Unknown Features", + "menu.issues.update_no_link_tooltip": "This feature needs to be updated but no download link is available. Check the mod page manually.", + "menu.issues.update_required": "Update Required", + "menu.issues.update_to_version_required": "Update to {version}+ Required", + "menu.issues.use_clear_issue_list": "Use 'Clear Issue List' to refresh after manual cleanup", + "menu.issues.use_open_features_folder": "Use 'Open Features Folder' to manually review INI files", + "menu.issues.use_open_logs": "Use 'Open Logs' to manually review the logs", + "menu.issues.use_open_shaders_directory": "Use 'Open Shaders Directory' to check for orphaned shader folders", + "menu.issues.warning_label": "WARNING:", + "menu.issues.wrong_version_desc": "The following features have version compatibility issues and were disabled automatically. Please check for any updates or if the feature is considered obsolete.", + "menu.issues.wrong_version_header": "Wrong Version Features", + "menu.restore_settings": "Restore Saved Settings", + "menu.save_settings": "Save Settings", + "menu.settings.auto_hide_feature_list": "Auto-hide Feature List", + "menu.settings.auto_hide_feature_list_tooltip": "Automatically hides the left feature list panel. Move cursor to the left edge to show it.", + "menu.settings.background_blur": "Background Blur", + "menu.settings.background_blur_tooltip": "Applies a blur effect to the background behind the menu window.", + "menu.settings.base_font_size": "Base Font Size", + "menu.settings.borders_and_separators": "Borders & Separators", + "menu.settings.button_text_align": "Button Text Align", + "menu.settings.button_text_align_tooltip": "Alignment applies when a button is larger than its text content.", + "menu.settings.cancel": "Cancel", + "menu.settings.cell_padding": "Cell Padding", + "menu.settings.center_header_title": "Center Header Title", + "menu.settings.center_header_title_tooltip": "Centers the Community Shaders title and logo in the header title bar", + "menu.settings.child_border_size": "Child Border Size", + "menu.settings.child_rounding": "Child Rounding", + "menu.settings.color_background": "Background", + "menu.settings.color_border": "Border", + "menu.settings.color_border_shadow": "Border Shadow", + "menu.settings.color_button": "Button", + "menu.settings.color_button_active": "Button (Active)", + "menu.settings.color_button_hovered": "Button (Hovered)", + "menu.settings.color_button_left": "Left", + "menu.settings.color_button_position": "ColorButtonPosition", + "menu.settings.color_button_right": "Right", + "menu.settings.color_check_mark": "Checkbox Checkmark", + "menu.settings.color_child_bg": "Child Window Background", + "menu.settings.color_current_hotkey": "Current Hotkey", + "menu.settings.color_default": "Default", + "menu.settings.color_disabled": "Disabled", + "menu.settings.color_docking_empty_bg": "Docking Empty Background", + "menu.settings.color_docking_preview": "Docking Preview", + "menu.settings.color_drag_drop_target": "Drag & Drop Target", + "menu.settings.color_drag_drop_target_bg": "Drag & Drop Target Background", + "menu.settings.color_error": "Error", + "menu.settings.color_frame_bg": "Frame Background", + "menu.settings.color_frame_bg_active": "Frame Background (Active)", + "menu.settings.color_frame_bg_hovered": "Frame Background (Hovered)", + "menu.settings.color_header": "Header", + "menu.settings.color_header_active": "Header (Active)", + "menu.settings.color_header_hovered": "Header (Hovered)", + "menu.settings.color_hovered": "Hovered", + "menu.settings.color_info": "Info", + "menu.settings.color_input_text_cursor": "Input Text Cursor", + "menu.settings.color_menu_bar_bg": "Menu Bar Background", + "menu.settings.color_minimized_transparency": "Minimized Transparency", + "menu.settings.color_modal_window_dim_bg": "Modal Window Dim Background", + "menu.settings.color_nav_cursor": "Navigation Cursor", + "menu.settings.color_nav_windowing_dim_bg": "Window Navigation Dim Background", + "menu.settings.color_nav_windowing_highlight": "Window Navigation Highlight", + "menu.settings.color_plot_histogram": "Plot Histogram", + "menu.settings.color_plot_histogram_hovered": "Plot Histogram (Hovered)", + "menu.settings.color_plot_lines": "Plot Lines", + "menu.settings.color_plot_lines_hovered": "Plot Lines (Hovered)", + "menu.settings.color_popup_bg": "Popup Background", + "menu.settings.color_resize_grip": "Resize Grip", + "menu.settings.color_resize_grip_active": "Resize Grip (Active)", + "menu.settings.color_resize_grip_hovered": "Resize Grip (Hovered)", + "menu.settings.color_restart_needed": "Restart Needed", + "menu.settings.color_scrollbar_bg": "Scrollbar Background", + "menu.settings.color_scrollbar_grab": "Scrollbar Grab", + "menu.settings.color_scrollbar_grab_active": "Scrollbar Grab (Active)", + "menu.settings.color_scrollbar_grab_hovered": "Scrollbar Grab (Hovered)", + "menu.settings.color_separator": "Separator", + "menu.settings.color_separator_active": "Separator (Active)", + "menu.settings.color_separator_hovered": "Separator (Hovered)", + "menu.settings.color_separator_line": "Separator Line", + "menu.settings.color_slider_grab": "Slider Grab", + "menu.settings.color_slider_grab_active": "Slider Grab (Active)", + "menu.settings.color_slider_input_bg": "Slider & Input Background", + "menu.settings.color_success": "Success", + "menu.settings.color_tab": "Tab", + "menu.settings.color_tab_dimmed": "Tab (Dimmed)", + "menu.settings.color_tab_dimmed_selected": "Tab (Dimmed Selected)", + "menu.settings.color_tab_dimmed_selected_overline": "Tab Dimmed Selected Overline", + "menu.settings.color_tab_hovered": "Tab (Hovered)", + "menu.settings.color_tab_selected": "Tab (Selected)", + "menu.settings.color_tab_selected_overline": "Tab Selected Overline", + "menu.settings.color_table_border_light": "Table Border (Light)", + "menu.settings.color_table_border_strong": "Table Border (Strong)", + "menu.settings.color_table_header_bg": "Table Header Background", + "menu.settings.color_table_row_bg": "Table Row Background", + "menu.settings.color_table_row_bg_alt": "Table Row Background (Alternate)", + "menu.settings.color_text": "Text", + "menu.settings.color_text_disabled": "Text (Disabled)", + "menu.settings.color_text_link": "Text Link", + "menu.settings.color_text_selected_bg": "Text Selection Background", + "menu.settings.color_title_bg": "Title Bar Background", + "menu.settings.color_title_bg_active": "Title Bar Background (Active)", + "menu.settings.color_title_bg_collapsed": "Title Bar Background (Collapsed)", + "menu.settings.color_tree_lines": "Tree Lines", + "menu.settings.color_unsaved_marker": "Unsaved Marker", + "menu.settings.color_warning": "Warning", + "menu.settings.color_window_bg": "Window Background", + "menu.settings.color_window_border": "Window Border", + "menu.settings.create_new_theme": "Create New Theme", + "menu.settings.create_new_theme_hint": "Create a new theme with your current settings:", + "menu.settings.create_theme": "Create Theme", + "menu.settings.cs_editor_toggle_key": "CS Editor Toggle Key:", + "menu.settings.delete_button": "Delete", + "menu.settings.delete_theme": "Delete", + "menu.settings.delete_theme_confirm_part1": "Are you sure you want to delete the theme '", + "menu.settings.delete_theme_confirm_part2": "'?\n\nThis will permanently remove the theme file. This cannot be undone.", + "menu.settings.delete_theme_title": "Delete Theme", + "menu.settings.delete_theme_tooltip": "Delete the theme file for '%s'. This cannot be undone.", + "menu.settings.description": "Description", + "menu.settings.description_tooltip": "Optional description for the theme", + "menu.settings.display_name": "Display Name", + "menu.settings.display_name_duplicate": "A theme with this display name already exists", + "menu.settings.display_name_tooltip": "Human-readable name shown in the dropdown", + "menu.settings.docking_splitter_size": "Docking Splitter Size", + "menu.settings.effect_toggle_key": "Effect Toggle Key:", + "menu.settings.effective_size": "Effective size: %.0f px", + "menu.settings.enable_async": "Enable Async", + "menu.settings.enable_async_tooltip": "Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!", + "menu.settings.enable_disk_cache": "Enable Disk Cache", + "menu.settings.enable_disk_cache_tooltip": "Disables loading shaders from disk and prevents saving compiled shaders to disk cache.", + "menu.settings.feature_header_scale": "Feature Header Scale", + "menu.settings.feature_header_scale_tooltip": "Scale multiplier for feature title text in the Settings tab.", + "menu.settings.feature_headings": "Feature Headings", + "menu.settings.file_label": "File: %s", + "menu.settings.filter_colors": "Filter colors", + "menu.settings.font": "Font", + "menu.settings.font_roles": "Font Roles", + "menu.settings.frame_border_size": "Frame Border Size", + "menu.settings.frame_padding": "Frame Padding", + "menu.settings.frame_rounding": "Frame Rounding", + "menu.settings.full_palette": "Full Palette", + "menu.settings.full_palette_tooltip": "Advanced color controls for detailed customization of all UI elements.", + "menu.settings.global_scale": "Global Scale", + "menu.settings.grab_min_size": "Grab Min Size", + "menu.settings.grab_rounding": "Grab Rounding", + "menu.settings.indent_spacing": "Indent Spacing", + "menu.settings.item_inner_spacing": "Item Inner Spacing", + "menu.settings.item_spacing": "Item Spacing", + "menu.settings.language": "Language", + "menu.settings.language_tooltip": "Select the display language for the Community Shaders interface.", + "menu.settings.last_shader_cache_duration": "Last shader cache build duration: %s", + "menu.settings.log_slider_deadzone": "Log Slider Deadzone", + "menu.settings.no_families": "No families", + "menu.settings.no_font_families_available": "No font families available", + "menu.settings.no_fonts_found": "No fonts found. Place .ttf files in Interface/CommunityShaders/Fonts/", + "menu.settings.no_style_variants": "No style variants found for this family.", + "menu.settings.no_styles": "No styles", + "menu.settings.open_themes_folder": "Open Themes Folder", + "menu.settings.open_themes_folder_tooltip": "Opens the Themes folder where you can add custom theme files.", + "menu.settings.overlay_toggle_key": "Overlay Toggle Key:", + "menu.settings.popup_border_size": "Popup Border Size", + "menu.settings.popup_rounding": "Popup Rounding", + "menu.settings.refresh": "Refresh", + "menu.settings.refresh_font_families": "Refresh Font Families", + "menu.settings.refresh_font_families_tooltip": "Rescan the Fonts directory after adding or removing font files.", + "menu.settings.require_shift_to_dock": "Require Shift to Dock", + "menu.settings.require_shift_to_dock_tooltip": "When enabled, you must hold Shift while dragging to dock/snap windows. Prevents accidental docking.", + "menu.settings.reset": "Reset", + "menu.settings.save_as_new_theme": "Save As New Theme", + "menu.settings.save_theme_button": "Save", + "menu.settings.save_theme_tooltip": "Updates the currently selected theme (%s) with your current settings", + "menu.settings.screenshot_key": "Screenshot Key:", + "menu.settings.scrollbar_opacity": "Scrollbar Opacity", + "menu.settings.scrollbar_rounding": "Scrollbar Rounding", + "menu.settings.scrollbar_size": "Scrollbar Size", + "menu.settings.section_borders": "Borders", + "menu.settings.section_docking": "Docking", + "menu.settings.section_language": "Language", + "menu.settings.section_layout": "Layout", + "menu.settings.section_main": "Main", + "menu.settings.section_rounding": "Rounding", + "menu.settings.section_tables": "Tables", + "menu.settings.section_widgets": "Widgets", + "menu.settings.selectable_text_align": "Selectable Text Align", + "menu.settings.selectable_text_align_tooltip": "Alignment applies when a selectable is larger than its text content.", + "menu.settings.selected_theme": "Selected Theme: ", + "menu.settings.separator_text_align": "Separator Text Align", + "menu.settings.separator_text_border_size": "Separator Text Border Size", + "menu.settings.separator_text_padding": "Separator Text Padding", + "menu.settings.shader_deduplicated": "Deduplicated", + "menu.settings.shader_disk_cache": "Disk cache", + "menu.settings.shader_failed": "Failed", + "menu.settings.shader_fast": "Fast (<2s)", + "menu.settings.shader_slow": "Slow (2-8s)", + "menu.settings.shader_very_slow": "Very slow (>=8s)", + "menu.settings.show_footer": "Show Footer", + "menu.settings.show_footer_tooltip": "Shows the footer with game version, swap chain, and GPU information at the bottom of the window", + "menu.settings.show_icon_buttons_in_header": "Show Icon Buttons in Header", + "menu.settings.show_icon_buttons_in_header_tooltip": "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\nWhen disabled: Shows as text buttons below the header", + "menu.settings.skip_clear_cache_dialogue": "Skip Clear Cache Dialogue", + "menu.settings.skip_clear_cache_dialogue_tooltip": "When checked, the shader cache will be cleared immediately without asking for confirmation.", + "menu.settings.skip_compilation_key": "Skip Compilation Key:", + "menu.settings.skip_unchanged_shaders": "Skip Unchanged Shaders", + "menu.settings.skip_unchanged_shaders_tooltip": "When enabled, each shader is recompiled from source only if its .hlsl file is newer than the cached .bin on disk. Shaders whose source has not changed are loaded directly from the disk cache, avoiding the full startup compilation cost. Useful for iterative testing: change a shader file and only that shader is rebuilt. Requires 'Enable Disk Cache' to be active.", + "menu.settings.status": "Status", + "menu.settings.tab_bar_border_size": "Tab Bar Border Size", + "menu.settings.tab_behavior": "Behavior", + "menu.settings.tab_border_size": "Tab Border Size", + "menu.settings.tab_colors": "Colors", + "menu.settings.tab_fonts": "Fonts", + "menu.settings.tab_interface": "Interface", + "menu.settings.tab_keybindings": "Keybindings", + "menu.settings.tab_rounding": "Tab Rounding", + "menu.settings.tab_shaders": "Shaders", + "menu.settings.tab_styling": "Styling", + "menu.settings.tab_themes": "Themes", + "menu.settings.table_angled_headers_angle": "Table Angled Headers Angle", + "menu.settings.theme_name": "Theme Name", + "menu.settings.theme_name_duplicate": "A theme with this name already exists", + "menu.settings.theme_name_required": "Theme name is required", + "menu.settings.theme_name_tooltip": "File name for the theme (without .json extension)", + "menu.settings.theme_preset": "Theme Preset", + "menu.settings.theme_save_info": "Theme changes are not saved with the global \"Save Settings\" button. Use the Themes tab to save changes to this theme.", + "menu.settings.theme_save_reminder": "If you changed the theme above, save your selection using the global \"Save Settings\" button.", + "menu.settings.theme_update_failed": "Failed to update theme", + "menu.settings.theme_updated_no_changes": "Theme updated successfully - no changes detected", + "menu.settings.theme_updated_with_changes": "Theme updated successfully! Changed settings:", + "menu.settings.thumb_active_opacity": "Thumb Active Opacity", + "menu.settings.thumb_active_opacity_tooltip": "Controls the opacity of the scrollbar thumb when being dragged.", + "menu.settings.thumb_hovered_opacity": "Thumb Hovered Opacity", + "menu.settings.thumb_hovered_opacity_tooltip": "Controls the opacity of the scrollbar thumb when hovered.", + "menu.settings.thumb_opacity": "Thumb Opacity", + "menu.settings.thumb_opacity_tooltip": "Controls the opacity of the scrollbar thumb (the draggable part).", + "menu.settings.toggle_key": "Toggle Key:", + "menu.settings.tooltip_hover_delay": "Tooltip Hover Delay", + "menu.settings.tooltip_hover_delay_tooltip": "Time in seconds to wait before a tooltip appears when hovering over an item.", + "menu.settings.track_opacity": "Track Opacity", + "menu.settings.track_opacity_tooltip": "Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar).", + "menu.settings.ui_behavior": "UI Behavior", + "menu.settings.use_custom_shaders": "Use Custom Shaders", + "menu.settings.use_custom_shaders_tooltip": "Disabling this effectively disables all features.", + "menu.settings.use_monochrome_cs_logo": "Use Monochrome CS Logo", + "menu.settings.use_monochrome_cs_logo_tooltip": "Uses monochrome version of the Community Shaders logo", + "menu.settings.use_monochrome_icons": "Use Monochrome Icons", + "menu.settings.use_monochrome_icons_tooltip": "Uses white monochrome icons that adapt to your theme's text color", + "menu.settings.use_resolution_based_font_size": "Use resolution-based font size", + "menu.settings.use_resolution_based_font_size_tooltip": "When enabled, the UI font size scales with your screen resolution. Disable to set a fixed size.", + "menu.settings.visual_effects": "Visual Effects", + "menu.settings.window_border_size": "Window Border Size", + "menu.settings.window_padding": "Window Padding", + "menu.settings.window_rounding": "Window Rounding", + "menu.setup.change_later": "You can change this later in General > Keybindings.", + "menu.setup.choose_hotkey": "Please choose a hotkey to access the menu:", + "menu.setup.cs_editor_unbound": "CS Editor hotkey unbound - chosen key uses Shift", + "menu.setup.cs_editor_will_be": "CS Editor hotkey will be: {key}", + "menu.setup.new_install_line1": "This appears to be a new install, update, or", + "menu.setup.new_install_line2": "reinstallation of Community Shaders.", + "menu.setup.press_any_key": "Press any key to set as toggle key...", + "menu.setup.press_to_close": "Press Escape or Enter to continue", + "menu.toggle_error_message": "Toggle Error Message", + "menu.toggle_error_message_tooltip": "Hide or show the shader failure message. Your installation is broken and will likely see errors in game. Please double check you have updated all features and that your load order is correct. See CommunityShaders.log for details and check the Nexus Mods page or Discord server.", + "menu.window_title": "Community Shaders {version}", + "menu.window_title_dev": "Community Shaders {version} [{build}]", + "overlay.modified_features": "Features that may have modified shaders detected. Check Feature Issues in the Menu.", + "overlay.shader_blocking_active": "Shader Blocking Active", + "overlay.uncompiled_warning": "WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading.", + "ui.cancel": "Cancel", + "ui.clear_cache": "Clear Cache", + "ui.clear_cache_confirm": "Are you sure you want to clear the shader cache?", + "ui.clear_cache_desc": "This will clear all compiled shaders from memory and disk cache (if enabled). Shaders will be recompiled when the game next encounters them.", + "ui.clear_shader_cache": "Clear Shader Cache?", + "ui.copy": "Copy", + "ui.dont_ask_again": "Don't ask me again", + "ui.search": "Search...", + "ui.search_features": "Search Features..." +} diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json new file mode 100644 index 0000000000..bdcf9974dc --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -0,0 +1,2093 @@ +{ + "_meta": { + "language": "简体中文", + "locale": "zh_CN", + "version": "1.0.0", + "authors": ["Community Shaders Team"] + }, + "common.active": "激活", + "common.inactive": "未激活", + "feature.category.characters": "角色", + "feature.category.display": "显示", + "feature.category.grass": "草地", + "feature.category.landscape_and_textures": "地形与纹理", + "feature.category.lighting": "光照", + "feature.category.materials": "材质", + "feature.category.other": "其他", + "feature.category.post_processing": "后处理", + "feature.category.sky": "天空", + "feature.category.utility": "工具", + "feature.category.water": "水面", + "feature.cloud_shadows.description": "为地形和物体投射逼真的云阴影,当云层掠过时产生动态的光影变化,增强氛围沉浸感。", + "feature.cloud_shadows.key_feature_1": "地形与物体的动态云阴影投射", + "feature.cloud_shadows.key_feature_2": "可配置的阴影不透明度,便于艺术控制", + "feature.cloud_shadows.key_feature_3": "与云层运动同步的实时阴影移动", + "feature.cloud_shadows.key_feature_4": "基于立方体贴图的精确阴影投射计算", + "feature.cloud_shadows.key_feature_5": "增强的天空渲染集成", + "feature.cloud_shadows.name": "云阴影", + "feature.cloud_shadows.opacity": "不透明度", + "feature.cloud_shadows.opacity_tooltip": "值越高,云阴影越暗。", + "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", + "feature.dynamic_cubemaps.color": "颜色", + "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", + "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", + "feature.dynamic_cubemaps.dynamic_cubemap_creator": "动态立方体贴图创建器", + "feature.dynamic_cubemaps.enable_creator": "启用创建器", + "feature.dynamic_cubemaps.enable_ssr": "启用屏幕空间反射", + "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", + "feature.dynamic_cubemaps.export": "导出", + "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", + "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", + "feature.dynamic_cubemaps.key_feature_3": "增强的水面反射,包含环境细节", + "feature.dynamic_cubemaps.key_feature_4": "支持标准和VR两种渲染模式", + "feature.dynamic_cubemaps.key_feature_5": "优化的立方体贴图推理与辐照度计算", + "feature.dynamic_cubemaps.name": "动态立方体贴图", + "feature.dynamic_cubemaps.roughness": "粗糙度", + "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", + "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", + "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", + "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", + "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", + "feature.exp_height_fog.dir_inscattering_anisotropy": "方向光内散射各向异性", + "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "通过Henyey-Greenstein相位函数控制内散射的不对称性。\n正值产生前向散射(太阳周围发光)。\n零为各向同性。负值产生后向散射。", + "feature.exp_height_fog.dir_inscattering_mul": "方向光内散射倍率", + "feature.exp_height_fog.disable_vanilla_fog": "禁用原版雾", + "feature.exp_height_fog.disable_vanilla_fog_tooltip": "完全禁用原版雾。仅应用指数高度雾。", + "feature.exp_height_fog.enable_exp_height_fog": "启用指数高度雾", + "feature.exp_height_fog.fog_density": "雾密度", + "feature.exp_height_fog.fog_height": "雾高度", + "feature.exp_height_fog.fog_height_falloff": "雾高度衰减", + "feature.exp_height_fog.fog_inscattering_color": "雾内散射颜色", + "feature.exp_height_fog.inscattering_cubemap_tint": "内散射立方体贴图色调", + "feature.exp_height_fog.original_fog_color_amount": "原始雾颜色量", + "feature.exp_height_fog.start_distance": "起始距离", + "feature.exp_height_fog.sunlight_attenuation": "阳光衰减量", + "feature.exp_height_fog.use_dynamic_cubemaps": "使用动态立方体贴图进行内散射", + "feature.exponential_height_fog.description": "添加逼真的高度雾效果,雾密度随高度变化,增强场景的大气深度和沉浸感。", + "feature.exponential_height_fog.key_feature_1": "新增指数高度雾效果", + "feature.exponential_height_fog.key_feature_2": "适配原版雾效设置", + "feature.exponential_height_fog.key_feature_3": "营造大气深度感", + "feature.exponential_height_fog.name": "指数高度雾", + "feature.extended_materials.complex_material": "复杂材质", + "feature.extended_materials.description": "扩展材质添加了包括视差遮蔽映射和复杂材质混合在内的高级材质效果。\n此功能可增强表面细节和深度感知,呈现更逼真的纹理。", + "feature.extended_materials.enable_complex_material": "启用复杂材质", + "feature.extended_materials.enable_complex_material_tooltip": "启用利用环境遮罩的复杂材质规范支持。包括视差贴图,以及更逼真的金属和镜面反射。对于环境遮罩中alpha通道无效的模组内容,可能导致纹理变形。", + "feature.extended_materials.enable_height_blending": "启用地形高度混合", + "feature.extended_materials.enable_height_blending_tooltip": "基于视差启用地形纹理混合。", + "feature.extended_materials.enable_legacy_terrain": "启用旧版地形", + "feature.extended_materials.enable_legacy_terrain_tooltip": "使用每张地形纹理的alpha通道启用地形视差。因此,所有地形纹理必须支持视差才能使效果正常工作。", + "feature.extended_materials.enable_parallax": "启用视差", + "feature.extended_materials.enable_parallax_tooltip": "在为视差制作的标准网格上启用视差效果。", + "feature.extended_materials.enable_parallax_warping_fix": "启用视差变形修复", + "feature.extended_materials.enable_parallax_warping_fix_tooltip": "启用修复,减少弯曲和平滑法线三角形上的视差缩放。", + "feature.extended_materials.enable_shadows": "启用阴影", + "feature.extended_materials.enable_shadows_tooltip": "使用视差时启用廉价软阴影。适用于所有方向光和点光源。", + "feature.extended_materials.extend_shadows": "扩展阴影", + "feature.extended_materials.extend_shadows_tooltip": "将视差阴影扩展到视差范围之外。对性能影响较小。", + "feature.extended_materials.key_feature_1": "视差遮蔽映射,增加深度感", + "feature.extended_materials.key_feature_2": "复杂材质混合", + "feature.extended_materials.key_feature_3": "地形高度图支持", + "feature.extended_materials.key_feature_4": "视差阴影", + "feature.extended_materials.key_feature_5": "基于高度的纹理混合", + "feature.extended_materials.name": "扩展材质", + "feature.extended_materials.parallax": "视差", + "feature.extended_materials.soft_shadows": "近似软阴影", + "feature.extended_translucency.alpha_mode_anisotropic_fabric": "3 - 各向异性织物", + "feature.extended_translucency.alpha_mode_disabled": "0 - 禁用", + "feature.extended_translucency.alpha_mode_isotropic_fabric": "2 - 各向同性织物、玻璃等", + "feature.extended_translucency.alpha_mode_rim_edge": "1 - 边缘光", + "feature.extended_translucency.blend_weight": "混合权重", + "feature.extended_translucency.blend_weight_tooltip": "控制效果应用于最终结果的混合权重。", + "feature.extended_translucency.default_material_model": "默认材质模型", + "feature.extended_translucency.default_material_model_tooltip": "各向异性半透明将根据您查看半透明表面的视角调整不透明度。\n - 禁用:无各向异性半透明,平坦Alpha。\n - 边缘光:无物理模型的简单边缘光效果,几何体边缘始终不透明,即使完全透明。\n - 各向同性织物:由单一方向编织的虚构织物,尊重法线贴图,也适用于玻璃面板层。\n - 各向异性织物:由切线和副法线方向编织的常见织物,忽略法线贴图。\n", + "feature.extended_translucency.description": "为薄织物和其他半透明材质提供逼真的渲染效果。\n支持多种材质模型,适用于不同类型的半透明表面。", + "feature.extended_translucency.key_feature_1": "多种半透明材质模型(边缘光、各向同性/各向异性织物)", + "feature.extended_translucency.key_feature_2": "逼真的织物半透明效果,支持方向光透射", + "feature.extended_translucency.key_feature_3": "通过NIF额外数据支持逐材质覆写", + "feature.extended_translucency.key_feature_4": "可配置的透明度与柔和度控制", + "feature.extended_translucency.key_feature_5": "性能优化的半透明计算", + "feature.extended_translucency.name": "扩展半透明", + "feature.extended_translucency.skinned_mesh_only": "仅蒙皮网格", + "feature.extended_translucency.skinned_mesh_only_tooltip": "控制此效果是否仅应用于蒙皮网格。如果在随机对象上看到不期望的效果,请勾选此选项。", + "feature.extended_translucency.softness": "柔和度", + "feature.extended_translucency.softness_tooltip": "控制Alpha增加的柔和度,增加柔和度会减少Alpha的增加量。", + "feature.extended_translucency.translucent_material": "半透明材质", + "feature.extended_translucency.transparency_increase": "透明度增加", + "feature.extended_translucency.transparency_increase_tooltip": "半透明材质会使材质平均更不透明,这可能与预期不同。降低Alpha以抵消此效果并增加输出的动态范围。", + "feature.grass_collision.description": "启用动态草地交互——当角色走过草地时,草会弯曲和摆动,营造更沉浸的环境反应。", + "feature.grass_collision.enable": "启用草地碰撞", + "feature.grass_collision.grass_collision": "草地碰撞", + "feature.grass_collision.key_feature_1": "角色移动带动实时草地变形", + "feature.grass_collision.key_feature_2": "最多支持256个同时交互的碰撞检测", + "feature.grass_collision.key_feature_3": "动态追踪角色位置以驱动草地响应", + "feature.grass_collision.key_feature_4": "性能优化的碰撞计算", + "feature.grass_collision.key_feature_5": "与现有草地渲染无缝集成", + "feature.grass_collision.name": "草地碰撞", + "feature.grass_lighting.basic_grass": "基础草地", + "feature.grass_lighting.brightness": "亮度", + "feature.grass_lighting.brightness_tooltip": "将草地纹理变暗,以便在新光照下看起来更好", + "feature.grass_lighting.complex_grass": "复杂草地", + "feature.grass_lighting.description": "通过改进的光照、高光和次表面散射,增强草地渲染效果。\n使草地看起来更自然,对光照条件反应更灵敏。", + "feature.grass_lighting.detection_header": "复杂草地检测", + "feature.grass_lighting.detection_threshold": "检测阈值", + "feature.grass_lighting.detection_threshold_tooltip": "检测复杂草地纹理的阈值。值越低越严格。", + "feature.grass_lighting.effects": "效果", + "feature.grass_lighting.glossiness": "光泽度", + "feature.grass_lighting.glossiness_tooltip": "高光光泽度。", + "feature.grass_lighting.key_feature_1": "增强的草地光照模型", + "feature.grass_lighting.key_feature_2": "草地上的高光反射", + "feature.grass_lighting.key_feature_3": "次表面散射效果", + "feature.grass_lighting.key_feature_4": "提升草地视觉质量", + "feature.grass_lighting.key_feature_5": "可配置的材质属性", + "feature.grass_lighting.lighting": "光照", + "feature.grass_lighting.name": "草地光照", + "feature.grass_lighting.override_complex": "覆盖复杂草地光照设置", + "feature.grass_lighting.override_complex_tooltip": "覆盖草地网格作者设置的参数。复杂草地作者可以为其草地网格定义亮度。然而,某些作者可能未考虑Community Shaders提供的额外光源。此选项将其草地设置视为非复杂草地。这是Community Shaders < 0.7.0中的默认行为", + "feature.grass_lighting.specular_desc": "复杂草地的高光", + "feature.grass_lighting.specular_strength": "高光强度", + "feature.grass_lighting.specular_strength_tooltip": "高光强度。", + "feature.grass_lighting.sss_amount": "SSS量", + "feature.grass_lighting.sss_tooltip": "次表面散射(SSS)量。柔和光照控制物体的均匀照明程度。背光照明照亮物体的背面。两者结合模拟光线穿过表面的传输。", + "feature.hair_specular.description": "提供更好的头发着色效果,具有逼真的高光反射和基于切线的光线交互,呈现更生动的头发外观。", + "feature.hair_specular.diffuse_multiplier": "漫反射倍率", + "feature.hair_specular.enable_self_shadow": "启用屏幕空间自阴影", + "feature.hair_specular.enable_self_shadow_tooltip": "为头发启用屏幕空间自阴影。\nMarschner头发模型在没有自阴影的情况下可能会有过亮的透射。\n", + "feature.hair_specular.enable_tangent_shift": "启用切线偏移", + "feature.hair_specular.enable_tangent_shift_tooltip": "启用使用切线偏移纹理来改变发丝上的高光变化。\n结果可能因使用的头发模型而异。\n", + "feature.hair_specular.enabled": "启用", + "feature.hair_specular.glossiness": "光泽度", + "feature.hair_specular.glossiness_tooltip": "控制头发的光泽度。\nKajiya-Kay模式中光泽度映射到高光指数。\nMarschner模式中控制头发表面的粗糙度。\n", + "feature.hair_specular.hair_base_color_multiplier": "头发基色倍率", + "feature.hair_specular.hair_mode": "头发模式", + "feature.hair_specular.hair_mode_tooltip": "选择要使用的头发着色模型。\nKajiya-Kay是模拟头发高光的经验模型。\nMarschner是更基于物理的模型,模拟头发光交互。\n两种模型都是各向异性的,支持基于切线的着色。\n没有自阴影时,Marschner可能因透射而显得过亮。\n", + "feature.hair_specular.hair_saturation": "头发饱和度", + "feature.hair_specular.indirect_diffuse_multiplier": "间接漫反射倍率", + "feature.hair_specular.indirect_specular_multiplier": "间接高光倍率", + "feature.hair_specular.key_feature_1": "逼真的头发高光反射", + "feature.hair_specular.key_feature_2": "增强的头发光泽度与饱和度控制", + "feature.hair_specular.key_feature_3": "独立的高光和漫反射光照倍率", + "feature.hair_specular.key_feature_4": "切线偏移纹理支持,实现多样的头发高光效果", + "feature.hair_specular.name": "头发高光", + "feature.hair_specular.primary_tangent_shift": "主高光切线偏移", + "feature.hair_specular.secondary_tangent_shift": "次高光切线偏移", + "feature.hair_specular.self_shadow_exponent": "自阴影指数", + "feature.hair_specular.self_shadow_scale": "自阴影缩放", + "feature.hair_specular.self_shadow_strength": "自阴影强度", + "feature.hair_specular.specular_multiplier": "高光倍率", + "feature.hair_specular.transmission": "透射", + "feature.hdr_display.advanced": "高级", + "feature.hdr_display.advanced_tooltip_enable_windows_hdr": "建议启用 Windows HDR,而不是在这里强制开启。", + "feature.hdr_display.advanced_tooltip_force_enable": "即使未检测到,也强制启用 HDR(不推荐)。", + "feature.hdr_display.cancel": "取消", + "feature.hdr_display.capable_display_windows_hdr_off": "支持 HDR 的显示器(Windows HDR 已关闭)", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_0": "你的显示器支持 HDR,但 Windows HDR 当前已关闭。", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_1": "请在 Windows 显示设置中启用 HDR,以允许自动检测。", + "feature.hdr_display.description": "为HDR显示器提供真正的高动态范围输出。", + "feature.hdr_display.display_detected": "检测到 HDR 显示器", + "feature.hdr_display.display_reports_max_nits": "显示器报告的最大亮度:%.0f 尼特", + "feature.hdr_display.display_reports_max_nits_tooltip_0": "该值由操作系统或驱动(DXGI MaxLuminance)报告,并非直接测量值。", + "feature.hdr_display.display_reports_max_nits_tooltip_1": "它可能来自 EDID 元数据,因此可能与真实高光峰值亮度不同。", + "feature.hdr_display.display_reports_max_nits_tooltip_2": "请把它当作初始参考值,并按需调节峰值亮度。", + "feature.hdr_display.dont_show_again": "不再显示此提示", + "feature.hdr_display.enable_hdr": "启用 HDR", + "feature.hdr_display.enable_hdr_tooltip": "启用 HDR 输出。在扩展动态范围下尽量保持与原版相近的视觉效果。", + "feature.hdr_display.enable_hdr_tooltip_not_detected": "未检测到 HDR 显示器。可使用“高级”按钮强制开启。", + "feature.hdr_display.enable_hdr_tooltip_windows_off": "显示器支持 HDR,但 Windows HDR 已关闭。请先在 Windows 显示设置中启用 HDR,然后重启游戏。", + "feature.hdr_display.enabled_without_detected_display": "HDR 已启用,但未检测到 HDR 显示器。", + "feature.hdr_display.exclusive_fullscreen_warning": "警告:检测到独占全屏模式。", + "feature.hdr_display.exclusive_fullscreen_warning_detail": "HDR 与独占全屏不兼容,可能无法正常工作。请切换到无边框窗口模式以获得正确的 HDR 支持。", + "feature.hdr_display.force_enable_hdr": "强制启用 HDR", + "feature.hdr_display.force_enable_hdr_confirm": "仅当你确实拥有 HDR 显示器,但它未被正确检测到时,才应继续。", + "feature.hdr_display.force_enable_hdr_detected_warning": "未在你的显示器上检测到 HDR。", + "feature.hdr_display.force_enable_hdr_sdr_warning": "如果你使用的是 SDR(标准动态范围)显示器,游戏画面会非常不正常。", + "feature.hdr_display.force_enable_hdr_warning": "警告:强制启用 HDR", + "feature.hdr_display.key_feature_1": "支持HDR10输出(10位色深),升级HDR缓冲区至16位,完全无裁剪的渲染管线以实现真正的HDR数值。", + "feature.hdr_display.key_feature_2": "基于Skyrim ISHDR路径的HDR感知色调映射(Reinhard/Hejl-Burgess-Dawson),在保留原版风格的同时改善HDR显示器上的高光处理。", + "feature.hdr_display.key_feature_3": "可配置的纸张白点和峰值亮度。", + "feature.hdr_display.name": "HDR 显示", + "feature.hdr_display.paper_white_nits": "纸白亮度(尼特)", + "feature.hdr_display.paper_white_tooltip_0": "控制 SDR 白色在 HDR 显示器上的显示亮度。", + "feature.hdr_display.paper_white_tooltip_1": "203 尼特是 ITU BT.2408 参考值。提高该值可获得更亮的画面。", + "feature.hdr_display.peak_brightness_nits": "峰值亮度(尼特)", + "feature.hdr_display.peak_brightness_tooltip_0": "显示器可输出的最大亮度。", + "feature.hdr_display.peak_brightness_tooltip_1": "请设置为与你显示器真实峰值亮度相匹配的数值。", + "feature.hdr_display.sdr_display_not_detected": "SDR 显示器(未检测到 HDR)", + "feature.hdr_display.ui_brightness_multiplier": "UI 亮度倍率", + "feature.hdr_display.ui_brightness_multiplier_tooltip_0": "在 HDR 模式下,UI 亮度 = 纸白亮度 x 此倍率。", + "feature.hdr_display.ui_brightness_multiplier_tooltip_1": "1.00x 表示 UI 以纸白亮度渲染。更高的值会让 UI 相对场景内容更亮。", + "feature.hdr_display.ui_brightness_multiplier_tooltip_2": "注意:主菜单和加载画面始终以纸白亮度渲染。", + "feature.hdr_display.warning_popup_title": "HDR 警告", + "feature.ibl.dalc_amount": "DALC量", + "feature.ibl.dalc_amount_tooltip": "将IBL亮度向游戏原版环境光(DALC)级别混合。\n0 = 不匹配(纯IBL亮度),1 = 完全匹配原版环境光。", + "feature.ibl.dalc_mode": "DALC模式", + "feature.ibl.dalc_mode_color_ratio": "颜色比例", + "feature.ibl.dalc_mode_dalc_plus_sky": "DALC + 天空", + "feature.ibl.dalc_mode_dalc_plus_sky_directional": "DALC + 天空(定向)", + "feature.ibl.dalc_mode_luminance_ratio": "亮度比例", + "feature.ibl.dalc_mode_tooltip": "DALC与IBL亮度比率的计算方式:\n亮度比:来自总亮度的标量比率(丢失DALC颜色色调)。\n颜色比:逐通道比率(保留DALC颜色色调)。\nDALC + 天空:使用原版环境光作为基础,天空IBL叠加。天光仅影响天空。\nDALC + 天空(方向性):相同,但天光也按方向降低原版环境光。", + "feature.ibl.description": "用基于物理的IBL替代游戏的环境光照,IBL从立方体贴图的球谐函数中推导得出。", + "feature.ibl.disable_in_interiors": "在室内禁用", + "feature.ibl.disable_in_interiors_tooltip": "在室内单元中禁用IBL。", + "feature.ibl.enable_ibl": "启用IBL", + "feature.ibl.enable_ibl_tooltip": "切换IBL。启用时,环境光来自立方体贴图球谐函数,而非原版系统。", + "feature.ibl.env_ibl_saturation": "环境IBL饱和度", + "feature.ibl.env_ibl_saturation_tooltip": "环境IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", + "feature.ibl.env_ibl_scale": "环境IBL缩放", + "feature.ibl.env_ibl_scale_tooltip": "环境IBL的强度倍率(来自动态立方体贴图)。\n控制周围环境对环境光照的贡献强度。", + "feature.ibl.fog_mix": "雾混合", + "feature.ibl.fog_mix_tooltip": "将雾颜色向IBL环境光颜色混合。\n0 = 原版雾,1 = 雾完全由IBL着色。", + "feature.ibl.key_feature_1": "将环境和天空立方体贴图投影为球谐函数(SH)以计算辐照度", + "feature.ibl.key_feature_2": "双IBL源:环境立方体贴图(动态立方体贴图)和Skyrim原生天空反射立方体贴图", + "feature.ibl.key_feature_3": "DALC亮度匹配,保持IBL与游戏环境光水平一致", + "feature.ibl.key_feature_4": "可配置的每源强度、饱和度、雾混合以及每天气覆写", + "feature.ibl.key_feature_5": "静态IBL回退纹理,用于世界外对象(如物品栏物品)", + "feature.ibl.name": "基于图像的光照", + "feature.ibl.preserve_fog_luminance": "保持雾亮度", + "feature.ibl.preserve_fog_luminance_tooltip": "当雾混合激活时,重新缩放IBL着色的雾以保持原始雾亮度。\n防止雾变得过亮或过暗。", + "feature.ibl.sky_ibl_saturation": "天空IBL饱和度", + "feature.ibl.sky_ibl_saturation_tooltip": "天空IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", + "feature.ibl.sky_ibl_scale": "天空IBL缩放", + "feature.ibl.sky_ibl_scale_tooltip": "天空IBL的强度倍率(来自游戏的原始反射立方体贴图)。\n控制天空对环境光照的贡献强度。", + "feature.ibl.use_static_ibl": "对世界外物体使用静态IBL", + "feature.ibl.use_static_ibl_tooltip": "对在游戏世界外渲染的物体(如物品栏物品、加载画面)使用预烘焙的静态IBL立方体贴图纹理。", + "feature.interior_sun.description": "允许太阳和月亮的光线和阴影照射到室内空间。", + "feature.interior_sun.force_double_sided": "强制双面渲染", + "feature.interior_sun.force_double_sided_tooltip": "在室内太阳阴影贴图渲染期间禁用背面剔除。将防止大部分通过未遮罩/未准备好的室内的漏光,性能成本较小。", + "feature.interior_sun.interior_shadow_distance": "室内阴影距离", + "feature.interior_sun.interior_shadow_distance_tooltip": "设置在室内渲染阴影的距离。较低值提供更高质量的阴影并改善性能,但可能导致远处室内空间照亮不正确。", + "feature.interior_sun.key_feature_1": "仅对明确启用的室内空间生效", + "feature.interior_sun.key_feature_2": "利用现有的太阳、月亮和天气系统", + "feature.interior_sun.key_feature_3": "包含强制双面渲染选项,适用于未准备的室内场景", + "feature.interior_sun.key_feature_4": "修复导致漏光的几何体裁剪问题", + "feature.interior_sun.name": "室内阳光", + "feature.inverse_square_lighting.description": "为光照实现额外的平方反比衰减,使光照衰减更加物理准确和逼真。", + "feature.inverse_square_lighting.key_feature_1": "基于强度自动计算光照半径", + "feature.inverse_square_lighting.key_feature_2": "光源在可配置的截止距离处平滑淡出,解决无限距离问题", + "feature.inverse_square_lighting.key_feature_3": "不修改任何现有光照", + "feature.inverse_square_lighting.key_feature_4": "需要使用开启了平方反比衰减的模组光源。", + "feature.inverse_square_lighting.key_feature_5": "与Light Placer完全集成", + "feature.inverse_square_lighting.name": "平方反比光照", + "feature.key_features": "主要特性:", + "feature.light_editor.active_shadow_lights": "活跃阴影光源:%u", + "feature.light_editor.base_object": "基础对象:0x%08X | %s", + "feature.light_editor.cell": "单元格:%s", + "feature.light_editor.color": "颜色", + "feature.light_editor.cutoff": "截止", + "feature.light_editor.disable_inverse_square_falloff_lights": "禁用平方反比衰减光源", + "feature.light_editor.disable_regular_falloff_lights": "禁用常规衰减光源", + "feature.light_editor.dynamic": "动态", + "feature.light_editor.filter_by": "过滤方式", + "feature.light_editor.flicker": "闪烁", + "feature.light_editor.flicker_slow": "缓慢闪烁", + "feature.light_editor.hemi_shadow": "半球阴影", + "feature.light_editor.intensity": "强度", + "feature.light_editor.inverse_square_light": "平方反比光源", + "feature.light_editor.ligh": "LIGH:0x%08X | %s", + "feature.light_editor.light_flags": "光源标志", + "feature.light_editor.lights": "光源", + "feature.light_editor.linear_light": "线性光源", + "feature.light_editor.memory_address": "内存地址:%p", + "feature.light_editor.negative": "负向", + "feature.light_editor.ni_light_name": "NiLight名称:%s", + "feature.light_editor.omni_shadow": "全向阴影", + "feature.light_editor.owner": "所有者:0x%08X | %s", + "feature.light_editor.owner_last_edited_by": "所有者最后编辑者:%s", + "feature.light_editor.portal_strict": "传送门严格", + "feature.light_editor.position_format": "X:%.2f,Y:%.2f,Z:%.2f", + "feature.light_editor.position_offset": "位置偏移", + "feature.light_editor.pulse": "脉冲", + "feature.light_editor.pulse_slow": "缓慢脉冲", + "feature.light_editor.radius": "半径", + "feature.light_editor.revert_changes": "还原更改", + "feature.light_editor.save_to_light_placer": "保存到Light Placer", + "feature.light_editor.save_to_light_placer_tooltip": "将当前设置保存到Light Placer JSON。", + "feature.light_editor.select_a_light": "选择光源", + "feature.light_editor.shadows_only": "仅阴影", + "feature.light_editor.shadows_only_tooltip": "仅显示带有HemiShadow或OmniShadow标志的光源。", + "feature.light_editor.size": "大小", + "feature.light_editor.sort_by": "排序方式", + "feature.light_editor.spotlight_not_applicable": "聚光灯:ISL光源类型标志不适用", + "feature.light_editor.total_lights": "总光源数:%u", + "feature.light_limit_fix.debug": "调试", + "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", + "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", + "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", + "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", + "feature.light_limit_fix.key_feature_1": "移除4光源限制", + "feature.light_limit_fix.key_feature_2": "无限动态光源", + "feature.light_limit_fix.key_feature_3": "提升光照质量", + "feature.light_limit_fix.key_feature_4": "增强视觉真实感", + "feature.light_limit_fix.key_feature_5": "增强视觉真实感", + "feature.light_limit_fix.light_limit_vis": "光源限制可视化", + "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", + "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", + "feature.light_limit_fix.name": "光源限制修复", + "feature.light_limit_fix.statistics": "统计", + "feature.linear_lighting.ambient_gamma": "环境伽马", + "feature.linear_lighting.ambient_multiplier": "环境倍率", + "feature.linear_lighting.blood_effects_multiplier": "血液效果倍率", + "feature.linear_lighting.color_gamma": "颜色伽马", + "feature.linear_lighting.deferred_effects_multiplier": "延迟效果倍率", + "feature.linear_lighting.description": "通过色彩空间转换来提高光照计算的准确性。", + "feature.linear_lighting.directional_light_multiplier": "方向光倍率", + "feature.linear_lighting.effect_gamma": "效果伽马", + "feature.linear_lighting.effect_lighting_multiplier": "效果光照倍率", + "feature.linear_lighting.effect_transparency_gamma": "效果透明度伽马", + "feature.linear_lighting.effects": "效果", + "feature.linear_lighting.emissive_color_gamma": "自发光颜色伽马", + "feature.linear_lighting.emissive_color_multiplier": "自发光颜色倍率", + "feature.linear_lighting.enable": "启用线性光照", + "feature.linear_lighting.fog_gamma": "雾伽马", + "feature.linear_lighting.fog_transparency_gamma": "雾透明度伽马", + "feature.linear_lighting.gamma_settings": "伽马设置", + "feature.linear_lighting.glowmap_gamma": "发光贴图伽马", + "feature.linear_lighting.glowmap_multiplier": "发光贴图倍率", + "feature.linear_lighting.key_feature_1": "可自定义的伽马校正", + "feature.linear_lighting.key_feature_2": "修正光照计算", + "feature.linear_lighting.key_feature_3": "使PBR真正生效", + "feature.linear_lighting.light_gamma": "光照伽马", + "feature.linear_lighting.membrane_effects_multiplier": "膜效果倍率", + "feature.linear_lighting.multipliers": "倍率", + "feature.linear_lighting.name": "线性光照", + "feature.linear_lighting.other_effects_multiplier": "其他效果倍率", + "feature.linear_lighting.point_light_multiplier": "点光源倍率", + "feature.linear_lighting.projected_effects_multiplier": "投射效果倍率", + "feature.linear_lighting.sky_gamma": "天空伽马", + "feature.linear_lighting.tab_advanced": "高级", + "feature.linear_lighting.tab_general": "通用", + "feature.linear_lighting.vanilla_diffuse_color_multiplier": "原版漫反射颜色倍率", + "feature.linear_lighting.vl_gamma": "体积光照伽马", + "feature.linear_lighting.water_gamma": "水伽马", + "feature.lod_blending.description": "在LOD对象与全细节对象之间提供无缝的视觉过渡,消除生硬的切换,创造平滑的视觉连续性。", + "feature.lod_blending.disable_terrain_vertex_colors": "禁用地形顶点颜色", + "feature.lod_blending.disable_terrain_vertex_colors_tooltip": "禁用附近地形上的顶点着色。建议与 xLODGen 生成、且 Vertex Color Intensity 设为 0 的地形 LOD 搭配使用。", + "feature.lod_blending.key_feature_1": "平滑的LOD对象亮度混合", + "feature.lod_blending.key_feature_2": "增强的地形LOD外观匹配", + "feature.lod_blending.key_feature_3": "针对雪景的LOD亮度调整", + "feature.lod_blending.key_feature_4": "可选的地形顶点颜色修改", + "feature.lod_blending.key_feature_5": "细节级别之间的无缝过渡", + "feature.lod_blending.lod_object_brightness": "LOD 物体亮度", + "feature.lod_blending.lod_object_gamma": "LOD 物体 Gamma", + "feature.lod_blending.lod_object_snow_brightness": "LOD 雪地物体亮度", + "feature.lod_blending.lod_object_snow_gamma": "LOD 雪地物体 Gamma", + "feature.lod_blending.lod_terrain_brightness": "LOD 地形亮度", + "feature.lod_blending.lod_terrain_gamma": "LOD 地形 Gamma", + "feature.lod_blending.name": "LOD混合", + "feature.perf_overlay.appearance": "外观", + "feature.perf_overlay.bg_opacity": "背景不透明度", + "feature.perf_overlay.clear_test_data": "清除测试数据", + "feature.perf_overlay.display_options": "显示选项", + "feature.perf_overlay.fps": "FPS:", + "feature.perf_overlay.frame_history_size": "帧历史大小", + "feature.perf_overlay.overlay_title": "性能叠加层", + "feature.perf_overlay.position": "位置:", + "feature.perf_overlay.post_fg_calculated": "帧生成后:计算计时(2倍帧生成前)", + "feature.perf_overlay.post_fg_fps": "帧生成后FPS:", + "feature.perf_overlay.post_fg_graph_tooltip": "FSR帧生成使用计算计时数据(2倍帧生成前)。\nDLSS帧生成提供测量计时数据。", + "feature.perf_overlay.raw_fps": "原始FPS:", + "feature.perf_overlay.reset_position": "重置位置", + "feature.perf_overlay.restore_defaults": "恢复默认值", + "feature.perf_overlay.restore_defaults_tooltip": "将性能叠加层设置恢复为默认值,包括图表、外观和更新间隔。", + "feature.perf_overlay.show_border": "显示边框", + "feature.perf_overlay.show_cs_passes": "显示CS渲染通道", + "feature.perf_overlay.show_draw_calls": "显示绘制调用", + "feature.perf_overlay.show_fps": "显示FPS计数器", + "feature.perf_overlay.show_frametime_graph": "显示帧时间图表", + "feature.perf_overlay.show_in_overlay": "在叠加层中显示", + "feature.perf_overlay.show_in_overlay_tooltip": "在单独的窗口中打开性能叠加层,即使主菜单关闭也保持打开。", + "feature.perf_overlay.show_post_fg_graph": "显示帧生成后帧时间图表", + "feature.perf_overlay.show_pre_fg_graph": "显示帧生成前帧时间图表", + "feature.perf_overlay.show_vram": "显示VRAM使用量", + "feature.perf_overlay.text_size": "文本大小", + "feature.perf_overlay.toggle_with": "切换键:", + "feature.perf_overlay.update_interval": "更新间隔", + "feature.perf_overlay.vram_not_available": "VRAM使用量:不可用", + "feature.perf_overlay.vram_usage": "VRAM使用量:", + "feature.performance_overlay.description": "实时性能监控系统,显示FPS、帧时间、绘制调用、显存使用量以及详细的着色器性能分析。", + "feature.performance_overlay.key_feature_1": "实时FPS和帧时间监控,可配置更新间隔", + "feature.performance_overlay.key_feature_2": "交互式绘制调用分析,按着色器类型展示性能细分", + "feature.performance_overlay.key_feature_3": "显存使用量监控,带可视化进度条", + "feature.performance_overlay.key_feature_4": "帧时间图表,用于帧生成前后的分析", + "feature.performance_overlay.key_feature_5": "A/B测试支持,对比不同配置的性能表现", + "feature.performance_overlay.key_feature_6": "颜色编码的性能指标,可自定义阈值", + "feature.performance_overlay.key_feature_7": "可移动的叠加窗口,位置持久保存", + "feature.performance_overlay.name": "性能叠加层", + "feature.render_doc.description": "提供应用内的RenderDoc捕获支持与便捷UI。", + "feature.render_doc.key_feature_1": "为捕获添加注释,可在RenderDoc UI中查看", + "feature.render_doc.key_feature_2": "打开捕获文件夹", + "feature.render_doc.key_feature_3": "捕获文件管理", + "feature.render_doc.name": "RenderDoc", + "feature.renderdoc.cancel": "取消", + "feature.renderdoc.capture_active": "RenderDoc捕获正在进行。", + "feature.renderdoc.capture_control": "捕获控制", + "feature.renderdoc.capture_control_tooltip": "手动捕获创建和基本控制", + "feature.renderdoc.capture_dir": "捕获目录:%s", + "feature.renderdoc.capture_dir_tooltip": "右键点击复制目录路径。", + "feature.renderdoc.capture_files": "捕获文件", + "feature.renderdoc.capture_files_tooltip": "查看和管理单个捕获文件", + "feature.renderdoc.capture_frames": "捕获帧数", + "feature.renderdoc.capture_frames_tooltip": "要捕获的连续帧数。1使用普通RenderDoc捕获;更高值使用TriggerMultiFrameCapture。", + "feature.renderdoc.capture_size": "捕获大小", + "feature.renderdoc.capture_size_tooltip": "捕获目录中所有捕获文件的总大小", + "feature.renderdoc.clear_all_captures": "清除所有捕获", + "feature.renderdoc.col_created": "创建时间", + "feature.renderdoc.col_filename": "文件名", + "feature.renderdoc.col_size": "大小", + "feature.renderdoc.comments_hint": "下一次捕获的附加注释(可选)", + "feature.renderdoc.comments_tooltip": "附加注释将追加到自动元数据中,并嵌入到.rdc文件中", + "feature.renderdoc.confirm_delete": "您确定要删除所有捕获文件吗?", + "feature.renderdoc.copy_dir_path": "复制目录路径", + "feature.renderdoc.create_capture": "创建捕获", + "feature.renderdoc.delete_size": "这将永久删除%u MB的捕获数据。", + "feature.renderdoc.disk_usage": "磁盘使用量", + "feature.renderdoc.disk_usage_tooltip": "监控捕获存储使用情况", + "feature.renderdoc.double_click_hint": "双击文件名以打开捕获文件", + "feature.renderdoc.enable_capture": "启用RenderDoc捕获", + "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", + "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", + "feature.renderdoc.hover_hint": "悬停在文件名上查看文件详情", + "feature.renderdoc.no_files": "未找到捕获文件。", + "feature.renderdoc.not_enough_space": "没有足够的可用磁盘空间来创建捕获。", + "feature.renderdoc.ok": "确定", + "feature.renderdoc.open_capture_dir": "打开捕获目录", + "feature.renderdoc.refresh_list": "刷新列表", + "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", + "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", + "feature.renderdoc.space_required": "至少需要{} MB的可用空间。", + "feature.renderdoc.yes_delete": "是,全部删除", + "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", + "feature.screen_space_gi.key_feature_1": "逼真的间接光照", + "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", + "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", + "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", + "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", + "feature.screen_space_gi.name": "屏幕空间GI", + "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", + "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", + "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", + "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", + "feature.screen_space_shadows.enable": "启用", + "feature.screen_space_shadows.enable_tooltip": "启用来自太阳/月亮方向的屏幕空间接触阴影。", + "feature.screen_space_shadows.general": "通用", + "feature.screen_space_shadows.key_feature_1": "增强的接触阴影", + "feature.screen_space_shadows.key_feature_2": "提升阴影细节", + "feature.screen_space_shadows.key_feature_3": "更好的阴影精度", + "feature.screen_space_shadows.key_feature_4": "精细尺度的阴影效果", + "feature.screen_space_shadows.key_feature_5": "可配置的阴影对比度", + "feature.screen_space_shadows.name": "屏幕空间阴影", + "feature.screen_space_shadows.sample_count": "采样数量倍率", + "feature.screen_space_shadows.sample_count_tooltip": "阴影射线采样数量的倍率。较高值以性能为代价增加阴影范围。适应渲染分辨率。", + "feature.screen_space_shadows.shadow_contrast": "阴影对比度", + "feature.screen_space_shadows.shadow_contrast_tooltip": "阴影过渡的对比度增强。较高值产生更硬的阴影边缘。", + "feature.screen_space_shadows.surface_thickness": "表面厚度", + "feature.screen_space_shadows.surface_thickness_tooltip": "阴影检测的假设表面厚度。较低值产生更薄、更精确的阴影。", + "feature.screen_space_shadows.vr_stereo_sync": "VR立体同步", + "feature.screen_space_shadows.vr_stereo_sync_tooltip": "通过双向重投影同步左右眼之间的阴影数据,并应用深度加权模糊以减少每只眼睛的噪点。使用最小混合,如果任一眼睛检测到遮挡物,则保留阴影。", + "feature.screenshot.apply_crop": "应用裁剪", + "feature.screenshot.async_note": "捕获和保存异步运行,不会让游戏停顿。", + "feature.screenshot.crop": "裁剪", + "feature.screenshot.folder": "文件夹", + "feature.screenshot.folder_tooltip": "相对路径相对于Skyrim安装目录解析。\n绝对路径(例如D:\\Captures)直接保存到该位置。", + "feature.screenshot.hdr_bit_depth": "HDR PNG 位深度", + "feature.screenshot.hdr_bit_depth_tooltip": "48 bpp RGB PNG 负载的量化位深。11位是较好的默认值;更高的值会增加文件大小,但收益递减。", + "feature.screenshot.hdr_note": "HDR 已启用:将显示帧保存为带有 HDR10 元数据的 PNG(48 bpp RGB,cICP/cLLi)。请使用支持 HDR 的查看器,如 Windows 照片(HDR 开启)或 Special K SKIF。", + "feature.screenshot.hotkey": "热键", + "feature.screenshot.hotkey_collision": "此热键与原版PrintScreen冲突;两者都会触发保存。\n在Skyrim.ini中设置bAllowScreenShot=0以抑制原版,或在上方选择不同的热键。", + "feature.screenshot.name": "截图", + "feature.screenshot.open": "打开", + "feature.screenshot.output": "输出", + "feature.screenshot.sdr_note": "启用HDR显示来捕捉有着HDR10元数据的HDR PNG截图。SDR和VR截图会使用以下选择的无损格式。", + "feature.screenshot.take_screenshot": "立即截图", + "feature.sky_sync.custom_angle": "自定义角度", + "feature.sky_sync.custom_angle_tooltip": "设置太阳轨迹的自定义角度。", + "feature.sky_sync.description": "将体积光照和阴影与天空中太阳和月亮的实际位置同步。", + "feature.sky_sync.enabled": "启用", + "feature.sky_sync.enabled_tooltip": "启用或禁用天空同步功能。", + "feature.sky_sync.key_feature_1": "修复太阳/月亮位置与光照方向不匹配的问题", + "feature.sky_sync.key_feature_2": "包含可配置的替代太阳路径,呈现更逼真戏剧化的光照", + "feature.sky_sync.key_feature_3": "根据可见性在太阳和月亮之间平滑切换光源", + "feature.sky_sync.key_feature_4": "月光源可在Masser、Secunda或最亮者之间切换", + "feature.sky_sync.key_feature_5": "基于月相自动计算月光强度", + "feature.sky_sync.key_feature_6": "修复玩家提升海拔时太阳在地平线上显得更高的问题", + "feature.sky_sync.min_shadow_elevation": "最小阴影仰角", + "feature.sky_sync.min_shadow_elevation_tooltip": "阳光设置的最小角度。限制阴影长度。更高 = 日落/日出时更短的阴影。", + "feature.sky_sync.moon_light_source": "月亮光源", + "feature.sky_sync.moon_light_source_brightest": "最亮者", + "feature.sky_sync.moon_light_source_masser": "Masser", + "feature.sky_sync.moon_light_source_secunda": "Secunda", + "feature.sky_sync.moon_light_source_tooltip": "选择夜晚投射阴影的月亮。", + "feature.sky_sync.name": "天空同步", + "feature.sky_sync.sun_path": "太阳路径", + "feature.sky_sync.sun_path_custom": "自定义", + "feature.sky_sync.sun_path_northern": "北侧天空", + "feature.sky_sync.sun_path_southern": "南侧天空", + "feature.sky_sync.sun_path_tooltip": "选择太阳穿越天空的轨迹。", + "feature.sky_sync.sun_path_vanilla": "原版", + "feature.sky_sync.sun_position_offsets": "太阳位置偏移", + "feature.sky_sync.sun_position_offsets_desc": "在日出/日落时移动太阳高度。重置天气以查看更改。", + "feature.sky_sync.sunrise_begin": "日出开始(小时)", + "feature.sky_sync.sunrise_begin_tooltip": "太阳开始升起的时间偏移。", + "feature.sky_sync.sunrise_end": "日出结束(小时)", + "feature.sky_sync.sunrise_end_tooltip": "太阳完成升起的时间偏移。", + "feature.sky_sync.sunset_begin": "日落开始(小时)", + "feature.sky_sync.sunset_begin_tooltip": "太阳开始落下的时间偏移。", + "feature.sky_sync.sunset_end": "日落结束(小时)", + "feature.sky_sync.sunset_end_tooltip": "太阳完成落下的时间偏移。", + "feature.sky_sync.use_alternate_sun_path": "使用备用太阳路径", + "feature.sky_sync.use_alternate_sun_path_tooltip": "根据时间和季节计算太阳位置,而非原版运动。", + "feature.skylighting.description": "通过计算天空遮蔽和方向光照,模拟逼真的环境照明,在户外环境中提供更精确自然的照明。", + "feature.skylighting.diffuse_min_visibility": "漫反射最小可见度", + "feature.skylighting.key_feature_1": "天空遮蔽计算,用于环境光照", + "feature.skylighting.key_feature_2": "基于环境几何体的方向性天空光照", + "feature.skylighting.key_feature_3": "增强的户外场景环境照明", + "feature.skylighting.key_feature_4": "支持变化的天空光照强度", + "feature.skylighting.key_feature_5": "与现有光照系统集成", + "feature.skylighting.max_zenith": "最大天顶角", + "feature.skylighting.max_zenith_tooltip": "较小的角度产生更集中的自上而下阴影。", + "feature.skylighting.min_visibility_desc": "最小可见度值。漫反射使物体变暗。镜面反射从反射中移除天空。", + "feature.skylighting.name": "天空光照", + "feature.skylighting.rebuild": "重建天光", + "feature.skylighting.rebuild_tooltip": "以下更改需要重建、加载屏幕或离开当前位置才能应用。", + "feature.skylighting.specular_min_visibility": "镜面反射最小可见度", + "feature.ssgi.ao_only": "仅AO", + "feature.ssgi.ao_power": "AO强度", + "feature.ssgi.ao_radius": "AO半径", + "feature.ssgi.ao_radius_tooltip": "较小的半径产生更紧密的AO。", + "feature.ssgi.blur": "模糊", + "feature.ssgi.blur_radius": "模糊半径", + "feature.ssgi.buffer_viewer": "缓冲区查看器", + "feature.ssgi.debug": "调试", + "feature.ssgi.denoising": "降噪", + "feature.ssgi.depth_fade_range": "深度渐隐范围", + "feature.ssgi.depth_fade_range_tooltip": "基于深度的效果渐隐的距离范围。", + "feature.ssgi.enabled": "启用", + "feature.ssgi.enabled_tooltip": "启用屏幕空间全局光照。禁用时,所有其他设置将被忽略。", + "feature.ssgi.extreme": "极致", + "feature.ssgi.extreme_tooltip": "全分辨率且干净。", + "feature.ssgi.full_res": "全分辨率", + "feature.ssgi.geometry_weight": "几何权重", + "feature.ssgi.geometry_weight_tooltip": "较高值使模糊对几何差异更敏感。", + "feature.ssgi.half_res": "半分辨率", + "feature.ssgi.hq_specular_il": "(实验性)HQ高光IL", + "feature.ssgi.hq_specular_il_tooltip": "实验性的高光GI,更准确但需要更多采样。不会被模糊。", + "feature.ssgi.il_distance_compensation": "IL距离补偿", + "feature.ssgi.il_distance_compensation_tooltip": "增亮/调暗更远的辐射度采样。", + "feature.ssgi.il_radius": "IL半径", + "feature.ssgi.il_radius_tooltip": "较大的半径产生更宽的IL。", + "feature.ssgi.il_saturation": "IL饱和度", + "feature.ssgi.il_source_brightness": "IL源亮度", + "feature.ssgi.indirect_lighting": "间接光照(IL)", + "feature.ssgi.low": "低", + "feature.ssgi.low_tooltip": "四分之一分辨率且模糊。", + "feature.ssgi.max_frame_accumulation": "最大帧累积", + "feature.ssgi.max_frame_accumulation_tooltip": "累积多少过去帧的结果。较高值噪点更少但可能导致鬼影。", + "feature.ssgi.min_screen_radius": "最小屏幕半径", + "feature.ssgi.min_screen_radius_tooltip": "以显示宽度比例表示的最小屏幕空间效果半径,防止远场AO过小。", + "feature.ssgi.movement_disocclusion": "运动去遮挡", + "feature.ssgi.movement_disocclusion_tooltip": "如果像素从上帧移动得太远,其辐射度将不会带入此帧。\n较低值更严格。", + "feature.ssgi.quality_performance": "质量/性能", + "feature.ssgi.quarter_res": "四分之一分辨率", + "feature.ssgi.reference": "参考", + "feature.ssgi.reference_tooltip": "参考模式。", + "feature.ssgi.shader_compile_error": "计算着色器编译失败!", + "feature.ssgi.show_advanced": "显示高级选项", + "feature.ssgi.slices": "切片", + "feature.ssgi.slices_tooltip": "采样采用多少个方向。\n控制噪点。", + "feature.ssgi.standard": "标准", + "feature.ssgi.standard_tooltip": "半分辨率且相对稳定。", + "feature.ssgi.steps_per_slice": "每切片步数", + "feature.ssgi.steps_per_slice_tooltip": "在每个方向上采样的数量。\n控制光照精度,以及效果半径较大时的噪点。", + "feature.ssgi.temporal_denoiser": "时间降噪器", + "feature.ssgi.thickness": "厚度", + "feature.ssgi.thickness_tooltip": "遮挡物的厚度。仅影响AO。", + "feature.ssgi.toggles": "开关", + "feature.ssgi.vanilla_ssao": "原版SSAO", + "feature.ssgi.vanilla_ssao_tooltip": "启用Skyrim内置的SSAO。使用SSGI时通常禁用此选项以避免双重变暗。", + "feature.ssgi.vanilla_ssao_tooltip_vr": "VR不支持原版SSAO。", + "feature.ssgi.view_resize": "视图调整大小", + "feature.ssgi.visual": "视觉", + "feature.ssgi.visual_il": "视觉 - IL", + "feature.sss.base_profile": "基础预设", + "feature.sss.blur_radius": "模糊半径", + "feature.sss.blur_radius_tooltip": "模糊半径。", + "feature.sss.burley": "Burley", + "feature.sss.burley_samples": "Burley采样数", + "feature.sss.enable_character_lighting": "启用角色光照", + "feature.sss.enable_character_lighting_tooltip": "原版功能,不推荐。", + "feature.sss.falloff": "衰减", + "feature.sss.human_profile": "人类预设", + "feature.sss.mean_free_path_color": "平均自由路径颜色", + "feature.sss.mean_free_path_color_tooltip": "控制光在红色、绿色和蓝色通道中进入次表面的距离。由平均自由路径距离缩放。", + "feature.sss.mean_free_path_distance": "平均自由路径距离", + "feature.sss.mean_free_path_distance_tooltip": "控制平均自由路径颜色进入次表面的距离。", + "feature.sss.separable_sss": "可分离SSS", + "feature.sss.settings": "设置", + "feature.sss.strength": "强度", + "feature.sss.thickness": "厚度", + "feature.sss.thickness_tooltip": "相对于深度的模糊半径。", + "feature.subsurface_scattering.description": "模拟光线穿透半透明材质(如皮肤),创造更逼真的角色光照效果。\n该技术使有机材质看起来更生动自然。", + "feature.subsurface_scattering.key_feature_1": "逼真的皮肤光照", + "feature.subsurface_scattering.key_feature_2": "光穿透模拟", + "feature.subsurface_scattering.key_feature_3": "为不同材质提供独立的配置文件", + "feature.subsurface_scattering.key_feature_4": "增强的角色外观", + "feature.subsurface_scattering.key_feature_5": "可配置的散射属性", + "feature.subsurface_scattering.name": "次表面散射", + "feature.terrain_blending.description": "提供地形与物体之间的无缝混合,消除物体与地面交汇处的生硬过渡,呈现更自然的景观。", + "feature.terrain_blending.enable": "启用地形混合", + "feature.terrain_blending.enable_tooltip": "启用地形与物体之间的无缝混合。", + "feature.terrain_blending.key_feature_1": "地形与物体的无缝混合过渡", + "feature.terrain_blending.key_feature_2": "高级深度缓冲区处理,实现平滑集成", + "feature.terrain_blending.key_feature_3": "支持替代地形渲染模式", + "feature.terrain_blending.key_feature_4": "针对复杂场景的多通道渲染优化", + "feature.terrain_blending.key_feature_5": "增强的地形交互视觉连续性", + "feature.terrain_blending.name": "地形混合", + "feature.terrain_helper.description": "为需要额外纹理槽和视差映射功能的地形模组提供增强的地形材质支持。", + "feature.terrain_helper.key_feature_1": "扩展的地形材质纹理槽支持", + "feature.terrain_helper.key_feature_2": "地形纹理的视差映射集成", + "feature.terrain_helper.key_feature_3": "自动地形材质检测与设置", + "feature.terrain_helper.key_feature_4": "支持高级地形修改", + "feature.terrain_helper.key_feature_5": "地形增强模组的兼容层", + "feature.terrain_helper.name": "地形辅助", + "feature.terrain_shadows.buffer_viewer": "缓冲区查看器", + "feature.terrain_shadows.debug": "调试", + "feature.terrain_shadows.description": "使用高度图数据为地形特征添加逼真的阴影投射,创造准确的地形阴影,增强深度感知和视觉真实感。", + "feature.terrain_shadows.enable_terrain_shadow": "启用地形阴影", + "feature.terrain_shadows.key_feature_1": "基于高度图的地形阴影计算", + "feature.terrain_shadows.key_feature_2": "基于太阳位置的动态阴影更新", + "feature.terrain_shadows.key_feature_3": "支持自定义高度图文件", + "feature.terrain_shadows.key_feature_4": "实时阴影预处理和计算", + "feature.terrain_shadows.key_feature_5": "与现有阴影系统集成", + "feature.terrain_shadows.name": "地形阴影", + "feature.terrain_variation.apply_to_lod_terrain": "应用到 LOD 地形", + "feature.terrain_variation.apply_to_lod_terrain_tooltip": "将该平铺修复应用到 LOD 地形对象。\n这有助于减少远处地形上可见的重复平铺效果。", + "feature.terrain_variation.description": "减少地形纹理的重复图案效果。\n通过为纹理采样添加变化来创造更自然的地形外观。", + "feature.terrain_variation.enable_tiling_fix": "启用地形平铺修复", + "feature.terrain_variation.enable_tiling_fix_tooltip": "减少地形纹理的重复平铺感。\n该技术通过在纹理采样中加入变化,让地形看起来更自然。", + "feature.terrain_variation.key_feature_1": "减少地形纹理的平铺感", + "feature.terrain_variation.key_feature_2": "可调节的基于距离的混合", + "feature.terrain_variation.key_feature_3": "提升地形视觉质量", + "feature.terrain_variation.key_feature_4": "与扩展材质视差兼容", + "feature.terrain_variation.name": "地形变化", + "feature.true_pbr.base_color_scale": "基础颜色缩放", + "feature.true_pbr.blue": "蓝", + "feature.true_pbr.coat": "镀层", + "feature.true_pbr.coat_color": "镀层颜色", + "feature.true_pbr.coat_roughness": "镀层粗糙度", + "feature.true_pbr.coat_specular_level": "镀层高光等级", + "feature.true_pbr.coat_strength": "镀层强度", + "feature.true_pbr.density_randomization": "密度随机化", + "feature.true_pbr.displacement_scale": "位移缩放", + "feature.true_pbr.enabled": "启用", + "feature.true_pbr.glint": "闪烁高光", + "feature.true_pbr.global_settings": "全局设置", + "feature.true_pbr.green": "绿", + "feature.true_pbr.inner_layer_displacement_offset": "内层位移偏移", + "feature.true_pbr.log_microfacet_density": "微表面密度对数", + "feature.true_pbr.material_density_randomization": "密度随机化", + "feature.true_pbr.material_glint": "闪烁高光", + "feature.true_pbr.material_glint_enabled": "启用", + "feature.true_pbr.material_log_microfacet_density": "微表面密度对数", + "feature.true_pbr.material_microfacet_roughness": "微表面粗糙度", + "feature.true_pbr.material_object": "材质对象", + "feature.true_pbr.material_object_settings": "材质对象设置", + "feature.true_pbr.material_save": "保存", + "feature.true_pbr.material_screenspace_scale": "屏幕空间缩放", + "feature.true_pbr.material_specular_level": "高光等级", + "feature.true_pbr.microfacet_roughness": "微表面粗糙度", + "feature.true_pbr.name": "True PBR", + "feature.true_pbr.red": "红", + "feature.true_pbr.reset_to_1_0": "重置为 1.0", + "feature.true_pbr.roughness": "粗糙度", + "feature.true_pbr.roughness_scale": "粗糙度缩放", + "feature.true_pbr.save": "保存", + "feature.true_pbr.screenspace_scale": "屏幕空间缩放", + "feature.true_pbr.specular_level": "高光等级", + "feature.true_pbr.subsurface": "次表面", + "feature.true_pbr.subsurface_color": "次表面颜色", + "feature.true_pbr.subsurface_opacity": "次表面不透明度", + "feature.true_pbr.texture_set": "纹理集", + "feature.true_pbr.texture_set_settings": "纹理集设置", + "feature.true_pbr.vertex_ao_strength": "顶点 AO 强度", + "feature.unified_water.debug": "调试", + "feature.unified_water.description": "通过用LOD0(近景水面)替换远处水面瓦片,提供全面的水面LOD不匹配修复。", + "feature.unified_water.error_water_cache_generation_failed_for_worldspaces_check": "错误:%d 个世界空间的水面缓存生成失败。请检查安装和 CommunityShaders.log", + "feature.unified_water.generating_water_cache": "正在生成水面缓存:", + "feature.unified_water.key_feature_1": "统一远景和近景水面的外观,统一所有光照视觉效果。", + "feature.unified_water.key_feature_2": "彻底且根本地解决水面LOD不匹配问题。", + "feature.unified_water.key_feature_3": "提供水面几何渲染的后台系统,支持更高级的水面效果。", + "feature.unified_water.key_feature_4": "通过使用优化的远距离水面网格来提升原版性能。", + "feature.unified_water.name": "统一水面", + "feature.unified_water.regenerate_caches": "重新生成缓存", + "feature.unified_water.regenerate_flowmap": "重新生成流图", + "feature.unified_water.use_optimised_meshes": "使用优化网格", + "feature.unified_water.use_optimised_meshes_tooltip": "使用三角面数显著更低的网格以提升性能,视觉质量无损。\n仅影响新创建的水体 - 需要切换位置或重启游戏才能生效。", + "feature.upscaling.backend_diagnostics": "后端诊断", + "feature.upscaling.description": "先进的超分辨率和帧生成技术,提升游戏性能。", + "feature.upscaling.dlss_model_preset": "DLSS模型预设", + "feature.upscaling.dlss_model_preset_default": "默认", + "feature.upscaling.dlss_model_preset_j": "预设 J", + "feature.upscaling.dlss_model_preset_k": "预设 K", + "feature.upscaling.dlss_model_preset_l": "预设 L", + "feature.upscaling.dlss_model_preset_m": "预设 M", + "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", + "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", + "feature.upscaling.fps_limit": "FPS限制", + "feature.upscaling.fps_limit_tooltip_1": "设置帧率上限目标。", + "feature.upscaling.fps_limit_tooltip_2": "起始值设置为比刷新率低2-3 FPS(例如120 Hz为117)。", + "feature.upscaling.frame_generation": "帧生成", + "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", + "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", + "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", + "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", + "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", + "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", + "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", + "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", + "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", + "feature.upscaling.key_feature_1": "DLSS(深度学习超采样)支持", + "feature.upscaling.key_feature_2": "FSR(FidelityFX超分辨率)支持", + "feature.upscaling.key_feature_3": "TAA(时间抗锯齿)支持", + "feature.upscaling.key_feature_4": "支持的系统可启用帧生成", + "feature.upscaling.low_latency_boost": "低延迟增强", + "feature.upscaling.low_latency_boost_tooltip_1": "保持GPU时钟更高,避免低GPU负载时的延迟尖峰。", + "feature.upscaling.low_latency_boost_tooltip_2": "在帧时间跳跃时有帮助;但会增加功耗和发热。", + "feature.upscaling.low_latency_mode": "低延迟模式", + "feature.upscaling.low_latency_mode_tooltip_1": "通过将CPU工作更紧密地与GPU同步来减少输入延迟。", + "feature.upscaling.low_latency_mode_tooltip_2": "可能略微降低最大FPS,但通常感觉响应更快。", + "feature.upscaling.marker_optimization_unavailable": "标记优化不可用(PCL未加载)。", + "feature.upscaling.method": "方法", + "feature.upscaling.method_none": "无", + "feature.upscaling.method_taa": "TAA", + "feature.upscaling.name": "超分辨率", + "feature.upscaling.native_inputs": "原生输入", + "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", + "feature.upscaling.preset_balanced": "平衡", + "feature.upscaling.preset_dlaa": "DLAA", + "feature.upscaling.preset_native_aa": "原生抗锯齿", + "feature.upscaling.preset_performance": "性能", + "feature.upscaling.preset_quality": "质量", + "feature.upscaling.preset_ultra_performance": "超级性能", + "feature.upscaling.reflex_blocked_by_fg": "当DX12帧生成交换链激活时,Reflex不可用。", + "feature.upscaling.reflex_not_available": "Reflex不可用。请确保sl.reflex.dll存在并重启。", + "feature.upscaling.sharpness": "锐度", + "feature.upscaling.streamline_logging": "Streamline日志记录", + "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", + "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", + "feature.upscaling.upscale_preset": "升频预设", + "feature.upscaling.upscaling_intermediates": "升频中间结果", + "feature.upscaling.use_fps_limit": "使用FPS限制", + "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", + "feature.upscaling.use_fps_limit_tooltip_2": "相比无上限渲染可以降低延迟。", + "feature.upscaling.use_markers_to_optimize": "使用标记优化", + "feature.upscaling.use_markers_to_optimize_tooltip_1": "使用帧标记以实现更紧密的Reflex计时。", + "feature.upscaling.use_markers_to_optimize_tooltip_2": "先尝试开启;如果在您的设置上造成卡顿则关闭。", + "feature.upscaling.view_resize": "视图调整大小", + "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", + "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", + "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", + "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", + "feature.volumetric_lighting.exterior_depth": "室外深度", + "feature.volumetric_lighting.exterior_height": "室外高度", + "feature.volumetric_lighting.exterior_quality": "室外质量", + "feature.volumetric_lighting.exterior_width": "室外宽度", + "feature.volumetric_lighting.interior_depth": "室内深度", + "feature.volumetric_lighting.interior_height": "室内高度", + "feature.volumetric_lighting.interior_quality": "室内质量", + "feature.volumetric_lighting.interior_width": "室内宽度", + "feature.volumetric_lighting.key_feature_1": "逼真的光散射", + "feature.volumetric_lighting.key_feature_2": "上帝射线和大气效果", + "feature.volumetric_lighting.key_feature_3": "独立的室内/室外设置", + "feature.volumetric_lighting.key_feature_4": "可配置的质量等级", + "feature.volumetric_lighting.key_feature_5": "增强的大气沉浸感", + "feature.volumetric_lighting.name": "体积光照", + "feature.volumetric_lighting.quality_custom": "自定义", + "feature.volumetric_lighting.quality_high": "高", + "feature.volumetric_lighting.quality_low": "低", + "feature.volumetric_lighting.quality_medium": "中", + "feature.volumetric_shadows.description": "为粒子和贴花等效果提供降采样的VSM阴影贴图。\n以最小的性能影响改善透明对象上的阴影质量。", + "feature.volumetric_shadows.key_feature_1": "降采样的VSM阴影", + "feature.volumetric_shadows.key_feature_2": "高斯模糊滤波", + "feature.volumetric_shadows.key_feature_3": "多级联支持", + "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", + "feature.volumetric_shadows.name": "体积阴影", + "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", + "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", + "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", + "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", + "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", + "feature.vr.key_feature_5": "可配置的遮挡剔除参数", + "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "调试", + "feature.vr_stereo.debug_pom_depth": "调试POM深度", + "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", + "feature.vr_stereo.enable": "启用", + "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", + "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", + "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", + "feature.vr_stereo.full_blend_distance": "完全混合距离", + "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", + "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", + "feature.vr_stereo.off": "关闭", + "feature.vr_stereo.pom_depth_scale": "POM深度缩放", + "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", + "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", + "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", + "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", + "feature.water_effects.key_feature_1": "逼真的水面焦散", + "feature.water_effects.key_feature_2": "增强的水下光照", + "feature.water_effects.key_feature_3": "水面上的动态光影图案", + "feature.water_effects.key_feature_4": "提升水面视觉保真度", + "feature.water_effects.key_feature_5": "大气水下效果", + "feature.water_effects.name": "水面效果", + "feature.wetness_effects.advanced": "高级", + "feature.wetness_effects.breadth": "广度", + "feature.wetness_effects.chance": "概率", + "feature.wetness_effects.chance_tooltip": "实际产生飞溅和涟漪的雨滴比例。较高的值会增加效果密度,但对性能影响最小。", + "feature.wetness_effects.climate_arctic_detail_0": "寒冷干燥的气候,降水量极少。", + "feature.wetness_effects.climate_arctic_detail_1": "最大降水量:约1.08毫米/小时(小雨)", + "feature.wetness_effects.climate_arctic_detail_2": "倍率:湿润度0.5倍,积水0.3倍,转换0.5倍。", + "feature.wetness_effects.climate_arctic_detail_3": "雨滴:30%概率,网格3.5单位,间隔0.4秒。", + "feature.wetness_effects.climate_arctic_detail_4": "性能影响:极低", + "feature.wetness_effects.climate_arctic_effect_0": "缓慢湿润累积(0.5倍)", + "feature.wetness_effects.climate_arctic_effect_1": "极少量积水形成(0.3倍)", + "feature.wetness_effects.climate_arctic_effect_2": "缓慢天气转换(0.5倍)", + "feature.wetness_effects.climate_arctic_effect_3": "稀疏降水(30%概率)", + "feature.wetness_effects.climate_coastal_detail_0": "海洋性气候,降水量大且频繁。", + "feature.wetness_effects.climate_coastal_detail_1": "最大降水量:约8.06毫米/小时(大雨)", + "feature.wetness_effects.climate_coastal_detail_2": "倍率:湿润度1.5倍,积水1.7倍,转换1.7倍。", + "feature.wetness_effects.climate_coastal_detail_3": "雨滴:80%概率,网格2.5单位,间隔0.25秒。", + "feature.wetness_effects.climate_coastal_detail_4": "性能影响:中等", + "feature.wetness_effects.climate_coastal_effect_0": "快速湿润累积(1.5倍)", + "feature.wetness_effects.climate_coastal_effect_1": "增强积水形成(1.7倍)", + "feature.wetness_effects.climate_coastal_effect_2": "快速天气转换(1.7倍)", + "feature.wetness_effects.climate_coastal_effect_3": "频繁降雨事件(80%概率)", + "feature.wetness_effects.climate_legacy_detail_0": "Riverwood的原始雨水效果值,提供完全向后兼容。", + "feature.wetness_effects.climate_legacy_detail_1": "最大降水量:约0.66毫米/小时(极小雨)", + "feature.wetness_effects.climate_legacy_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", + "feature.wetness_effects.climate_legacy_detail_3": "雨滴:30%概率,网格4.0单位,间隔0.5秒。", + "feature.wetness_effects.climate_legacy_detail_4": "性能影响:极低(基准线)", + "feature.wetness_effects.climate_legacy_effect_0": "原始湿润累积(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_1": "原始积水形成(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_2": "原始天气转换(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_3": "原始雨滴频率(1.0倍)", + "feature.wetness_effects.climate_monsoon_detail_0": "热带/季风气候,极端降水量。", + "feature.wetness_effects.climate_monsoon_detail_1": "最大降水量:约22毫米/小时(极端)", + "feature.wetness_effects.climate_monsoon_detail_2": "倍率:湿润度2.0倍,积水2.5倍,转换2.0倍。", + "feature.wetness_effects.climate_monsoon_detail_3": "雨滴:100%概率,网格2.0单位,间隔0.2秒。", + "feature.wetness_effects.climate_monsoon_detail_4": "天际的小雨将无法匹配湿润效果。", + "feature.wetness_effects.climate_monsoon_detail_5": "性能影响:高(可能影响GPU性能)", + "feature.wetness_effects.climate_monsoon_effect_0": "极速湿润累积(2.0倍)", + "feature.wetness_effects.climate_monsoon_effect_1": "最大积水形成(2.5倍)", + "feature.wetness_effects.climate_monsoon_effect_2": "极动态天气(2.0倍)", + "feature.wetness_effects.climate_monsoon_effect_3": "最大雨滴频率(100%概率)", + "feature.wetness_effects.climate_nordic_detail_0": "平衡的温带北欧气候。", + "feature.wetness_effects.climate_nordic_detail_1": "最大降水量:约3.35毫米/小时(中雨)", + "feature.wetness_effects.climate_nordic_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", + "feature.wetness_effects.climate_nordic_detail_3": "雨滴:100%概率,网格3.0单位,间隔1.0秒。", + "feature.wetness_effects.climate_nordic_detail_4": "性能影响:低", + "feature.wetness_effects.climate_nordic_effect_0": "标准湿润累积(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_1": "标准积水形成(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_2": "标准天气转换(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_3": "中等雨滴频率(100%概率)", + "feature.wetness_effects.climate_preset": "气候预设", + "feature.wetness_effects.climate_preset_arctic": "北极苔原", + "feature.wetness_effects.climate_preset_arctic_desc": "寒冷干燥的北极气候(小雨)", + "feature.wetness_effects.climate_preset_coastal": "温带沿海", + "feature.wetness_effects.climate_preset_coastal_desc": "海洋性气候(大雨)", + "feature.wetness_effects.climate_preset_custom": "自定义", + "feature.wetness_effects.climate_preset_custom_desc": "用户自定义设置", + "feature.wetness_effects.climate_preset_legacy": "旧版", + "feature.wetness_effects.climate_preset_legacy_desc": "原始雨水效果值(极小雨)", + "feature.wetness_effects.climate_preset_monsoon": "季风/极端", + "feature.wetness_effects.climate_preset_monsoon_desc": "极端季风气候(暴雨)", + "feature.wetness_effects.climate_preset_nordic": "北欧(默认)", + "feature.wetness_effects.climate_preset_nordic_desc": "平衡的北欧气候(中雨)", + "feature.wetness_effects.climate_preset_unknown": "未知", + "feature.wetness_effects.climate_presets": "气候预设", + "feature.wetness_effects.custom_preset_tooltip_0": "自定义设置 - 您已修改预设值。", + "feature.wetness_effects.custom_preset_tooltip_1": "在上方选择一个预设以应用预定义的气候设置。", + "feature.wetness_effects.debug": "调试", + "feature.wetness_effects.description": "添加逼真的湿润效果,包括基于降雨的表面湿润、积水形成、岸边湿润以及动态雨滴效果,增强天气沉浸感。", + "feature.wetness_effects.effect_range": "效果范围", + "feature.wetness_effects.effect_range_tooltip": "雨滴效果的作用范围", + "feature.wetness_effects.effects": "效果:", + "feature.wetness_effects.enable_interior_exterior_override": "启用室内/室外覆写", + "feature.wetness_effects.enable_puddle_override": "启用积水覆写", + "feature.wetness_effects.enable_rain_override": "启用降雨覆写", + "feature.wetness_effects.enable_raindrop_effects": "启用雨滴效果", + "feature.wetness_effects.enable_ripples": "启用涟漪", + "feature.wetness_effects.enable_ripples_tooltip": "在积水上启用圆形涟漪,在较小程度上也在其他湿润表面上生效", + "feature.wetness_effects.enable_splashes": "启用飞溅", + "feature.wetness_effects.enable_splashes_tooltip": "在干燥表面上启用小型湿润飞溅效果。", + "feature.wetness_effects.enable_vanilla_ripples": "启用原版涟漪", + "feature.wetness_effects.enable_vanilla_ripples_controlled": "启用原版涟漪 - 由Splashes of Storms控制", + "feature.wetness_effects.enable_wetness": "启用湿润效果", + "feature.wetness_effects.enable_wetness_override": "启用湿润覆写", + "feature.wetness_effects.enable_wetness_tooltip": "在水边和下雨时启用表面湿润效果。", + "feature.wetness_effects.grid_size": "网格尺寸", + "feature.wetness_effects.grid_size_tooltip_0": "雨滴放置的空间网格尺寸(越小=更多网格单元,更高的GPU开销)", + "feature.wetness_effects.grid_size_tooltip_1": "这是对性能最敏感的选项。仅在需要更逼真效果时才降低此值。", + "feature.wetness_effects.interior_exterior_override_tooltip": "如果禁用,将仅使用室外值。", + "feature.wetness_effects.interval": "间隔", + "feature.wetness_effects.interval_tooltip": "检查雨滴效果的频率(越低越频繁,中等性能影响)", + "feature.wetness_effects.key_feature_1": "基于天气条件的动态表面湿润", + "feature.wetness_effects.key_feature_2": "逼真的积水形成与岸边湿润效果", + "feature.wetness_effects.key_feature_3": "带动画飞溅和涟漪的雨滴效果", + "feature.wetness_effects.key_feature_4": "可配置的湿润强度和天气转换速度", + "feature.wetness_effects.key_feature_5": "支持皮肤湿润和特定材质响应", + "feature.wetness_effects.lifetime": "生命周期", + "feature.wetness_effects.max_radius": "最大半径", + "feature.wetness_effects.meters_format": "{:.2f} 米", + "feature.wetness_effects.min_radius": "最小半径", + "feature.wetness_effects.min_rain_wetness": "最小降雨湿润度", + "feature.wetness_effects.min_rain_wetness_tooltip": "物体因雨水变湿的最小程度。", + "feature.wetness_effects.name": "湿润效果", + "feature.wetness_effects.open_feature": "打开 {}", + "feature.wetness_effects.open_installed_feature_tooltip": "打开已安装的 %s 功能", + "feature.wetness_effects.portion_of_grid_size": "作为网格尺寸的比例。", + "feature.wetness_effects.puddle_max_angle": "积水最大角度", + "feature.wetness_effects.puddle_max_angle_tooltip": "表面需要多平才能形成积水。", + "feature.wetness_effects.puddle_min_wetness": "积水最小湿润度", + "feature.wetness_effects.puddle_min_wetness_tooltip": "积水开始形成时的湿润度值。", + "feature.wetness_effects.puddle_radius": "积水半径", + "feature.wetness_effects.puddle_radius_tooltip": "用于确定积水大小和位置的半径", + "feature.wetness_effects.puddle_wetness": "积水湿润度", + "feature.wetness_effects.puddle_wetness_in_exterior": "积水湿润度 室内/室外", + "feature.wetness_effects.radius": "半径", + "feature.wetness_effects.rain_in_exterior": "降雨 室内/室外", + "feature.wetness_effects.rain_wetness": "降雨湿润度", + "feature.wetness_effects.raindrop_effects": "雨滴效果", + "feature.wetness_effects.raindrops": "雨滴", + "feature.wetness_effects.raindrops_help": "在每个间隔内,每个网格单元中放置一个雨滴。\n只有设定比例的雨滴会实际触发飞溅和涟漪。\n", + "feature.wetness_effects.ripples": "涟漪", + "feature.wetness_effects.shore_range": "岸边范围", + "feature.wetness_effects.shore_range_tooltip": "岸边湿润效果影响水体的最大距离", + "feature.wetness_effects.shore_wetness": "岸边湿润度", + "feature.wetness_effects.skin_wetness": "皮肤湿润度", + "feature.wetness_effects.skin_wetness_tooltip": "雨天时角色皮肤和头发的湿润程度。", + "feature.wetness_effects.splashes": "飞溅", + "feature.wetness_effects.strength": "强度", + "feature.wetness_effects.vanilla_ripples_tooltip_0": "启用默认涟漪(例如Ripples01)。", + "feature.wetness_effects.vanilla_ripples_tooltip_1": "禁用可能要到下次天气变化时才生效。", + "feature.wetness_effects.weather_transition_speed": "天气转换速度", + "feature.wetness_effects.weather_transition_speed_tooltip": "下雨时湿润效果出现的速度以及雨停后干燥的速度。", + "feature.wetness_effects.wetness_effects": "湿润效果", + "feature.wetness_effects.wetness_in_exterior": "湿润度 室内/室外", + "menu.advanced.active_shaders": "活跃着色器", + "menu.advanced.active_shaders_tooltip": "最近帧中使用过的着色器列表。在上方启用着色器拦截可使用热键循环浏览并拦截着色器进行调试。约1秒未使用的着色器将从此列表中移除。", + "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", + "menu.advanced.addresses": "地址", + "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", + "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", + "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", + "menu.advanced.avoid_flow_control": "避免流控制", + "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.background_compiler_threads": "后台编译器线程", + "menu.advanced.background_compiler_threads_tooltip": "游戏过程中用于编译着色器的线程数。默认为性能核心的一半,以避免影响渲染线程。较高值可更快完成编译,但可能导致卡顿。", + "menu.advanced.block_next": "拦截下一个:", + "menu.advanced.block_previous": "拦截上一个:", + "menu.advanced.blocked_shader": "已拦截:%s", + "menu.advanced.change_shader_block_next": "更改##ShaderBlockNext", + "menu.advanced.change_shader_block_prev": "更改##ShaderBlockPrev", + "menu.advanced.clear_shader_cache": "清除着色器缓存", + "menu.advanced.clear_shader_cache_tooltip": "从内存中清除所有已编译的着色器。下次使用时强制重新编译所有着色器。", + "menu.advanced.click_to_block": "左键点击拦截此着色器", + "menu.advanced.click_to_unblock": "左键点击取消拦截此着色器", + "menu.advanced.column_class": "类别", + "menu.advanced.column_class_tooltip": "着色器类别", + "menu.advanced.column_descriptor": "描述符", + "menu.advanced.column_descriptor_tooltip": "着色器描述符", + "menu.advanced.column_frame_pct": "帧百分比", + "menu.advanced.column_frame_pct_tooltip": "此帧中绘制调用的百分比", + "menu.advanced.column_key": "键", + "menu.advanced.column_key_tooltip": "着色器键", + "menu.advanced.column_type": "类型", + "menu.advanced.column_type_tooltip": "着色器类型", + "menu.advanced.compiler_threads": "编译器线程", + "menu.advanced.compiler_threads_tooltip": "启动时用于编译着色器的线程数。默认为所有逻辑核心减去一个以留出系统开销(包含E核)。较高值可更快完成编译,但可能降低系统响应性。", + "menu.advanced.compute": "计算", + "menu.advanced.compute_tooltip": "替换计算着色器。设为false时将禁用上述类型的自定义计算着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.copy_info": "复制信息", + "menu.advanced.copy_info_tooltip": "将包含缓存路径的完整着色器信息复制到剪贴板", + "menu.advanced.copy_key": "复制键", + "menu.advanced.dump_ini_settings": "导出INI设置", + "menu.advanced.dump_shaders": "导出着色器", + "menu.advanced.dump_shaders_tooltip": "在启动时导出着色器。仅在逆向着色器时使用。普通用户无需此功能。", + "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", + "menu.advanced.enable_file_watcher": "启用文件监视器", + "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", + "menu.advanced.enable_shader_blocking": "启用着色器拦截", + "menu.advanced.enable_shader_blocking_tooltip": "启用热键以循环浏览并拦截单个着色器,用于调试目的。", + "menu.advanced.frame_annotations": "帧注释", + "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", + "menu.advanced.half_precision": "半精度(部分精度)", + "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.infinite_core_efficiency": "无限核心效率", + "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", + "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", + "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", + "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", + "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", + "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", + "menu.advanced.log_level": "日志级别", + "menu.advanced.log_level_critical": "严重", + "menu.advanced.log_level_debug": "调试", + "menu.advanced.log_level_err": "错误", + "menu.advanced.log_level_info": "信息", + "menu.advanced.log_level_off": "关闭", + "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", + "menu.advanced.log_level_trace": "跟踪", + "menu.advanced.log_level_warn": "警告", + "menu.advanced.makespan_label": "完工时间(T_p)", + "menu.advanced.makespan_metric": "完工时间(T_p):%s", + "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", + "menu.advanced.open_logs": "打开日志", + "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", + "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", + "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", + "menu.advanced.pixel": "像素", + "menu.advanced.pixel_tooltip": "替换像素着色器。设为false时将禁用上述类型的自定义像素着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.press_key_shader_block_next": "按下任意键设置着色器拦截下一个...", + "menu.advanced.press_key_shader_block_prev": "按下任意键设置着色器拦截上一个...", + "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", + "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", + "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", + "menu.advanced.relative_bar_format": "{}({:.1f}%)", + "menu.advanced.relative_durations": "相对持续时间(归一化)", + "menu.advanced.replace_original_shaders": "替换原始着色器", + "menu.advanced.shader_blocking_active": "着色器拦截已激活", + "menu.advanced.shader_class_label": "类别:%s", + "menu.advanced.shader_compiler_stats": "着色器编译器:{}", + "menu.advanced.shader_debug_header": "着色器调试", + "menu.advanced.shader_defines": "着色器定义", + "menu.advanced.shader_defines_tooltip": "着色器编译器的定义。以分号\";\"分隔。用空格清除。更改后需重建着色器。计算着色器需要重启才能重新编译。", + "menu.advanced.shader_descriptor": "描述符:0x%X", + "menu.advanced.shader_row_tooltip": "类型:{}\n类别:{}\n描述符:0x{:X}\n键:{}\n\n{}", + "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", + "menu.advanced.shader_type_label": "类型:%s", + "menu.advanced.span_label": "跨度(S)", + "menu.advanced.span_metric": "跨度(S,最长):%s", + "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", + "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", + "menu.advanced.statistics": "统计", + "menu.advanced.stop_blocking": "停止拦截##Section", + "menu.advanced.tab_developer": "开发者", + "menu.advanced.tab_disable_at_boot": "启动时禁用", + "menu.advanced.tab_logging": "日志记录", + "menu.advanced.tab_shader_debug": "着色器调试", + "menu.advanced.tab_testing": "测试", + "menu.advanced.test_conditions": "测试条件", + "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", + "menu.advanced.vertex": "顶点", + "menu.advanced.vertex_tooltip": "替换顶点着色器。设为false时将禁用上述类型的自定义顶点着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.work_label": "工作量(W)", + "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", + "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", + "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", + "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", + "menu.clear_shader_cache": "清除着色器缓存", + "menu.clear_shader_cache_tooltip": "清除着色器缓存和磁盘缓存(如果启用)。\n着色器缓存是在运行时替换原版着色器的已编译着色器集合。\n磁盘缓存是磁盘上已编译着色器的集合。清除后意味着着色器仅在游戏再次遇到它们时才重新编译。", + "menu.disable_at_boot_desc": "选择要在启动时禁用的功能。这与删除feature.ini文件相同。重新启用需要重启。", + "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", + "menu.faq.a2": "每个功能都可以在左侧边栏菜单中找到。点击任何功能即可访问其设置。大多数功能包含预设和详细的工具提示,帮助您了解每个设置的作用。", + "menu.faq.a3": "功能可能因硬件不兼容、依赖项缺失或与其他模组冲突而无法加载。请查看\"功能问题\"选项卡,了解有关任何有问题的功能的详细信息。", + "menu.faq.a4": "着色器失败通常由混合文件版本引起。请确保所有功能均为最新,并避免混合测试版本或过时版本的文件。请查看\"功能问题\"选项卡和/或Wiki了解更多信息。更新您的功能并移除任何过时的功能。", + "menu.faq.a5": "首先启用性能叠加层来监控您的FPS。考虑禁用屏幕空间GI等占用资源的功能或降低质量设置。\"显示\"选项卡还包含可以提升性能的升频选项。", + "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", + "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", + "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", + "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", + "menu.faq.q1": "什么是Community Shaders?", + "menu.faq.q2": "如何配置功能?", + "menu.faq.q3": "为什么有些功能无法加载?", + "menu.faq.q4": "编译时出现“着色器失败”?", + "menu.faq.q5": "如何提升性能?", + "menu.faq.q6": "Community Shaders与ENB兼容吗?", + "menu.faq.q7": "菜单热键无效!", + "menu.faq.q8": "我想帮助开发Community Shaders。", + "menu.faq.q9": "Community Shaders是开源的吗?", + "menu.faq.title": "常见问题解答", + "menu.features": "功能", + "menu.features.advanced": "高级", + "menu.features.also_feature": "另见:%s", + "menu.features.apply_override": "应用覆盖", + "menu.features.available_after_restart": "此功能将在重启后可用。", + "menu.features.boot_toggle_tooltip": "切换启动时加载功能。\n当前状态:%s\n需要重启才能使更改生效。\n禁用可消除性能影响。", + "menu.features.cannot_apply_overrides_scene": "在场景特定设置激活时无法应用覆盖。\n请先暂停此功能的场景设置。", + "menu.features.click_to_navigate": "点击导航到%s", + "menu.features.col_constrained_by": "受限于", + "menu.features.col_forced_to": "强制为", + "menu.features.col_impacted_feature": "受影响的功能", + "menu.features.col_setting": "设置", + "menu.features.constraints_explanation": "这些设置在其各自的功能菜单中因约束激活而被禁用。调整约束功能以移除它们。", + "menu.features.disabled": "禁用", + "menu.features.display": "显示", + "menu.features.dont_show_warning": "不再显示此警告", + "menu.features.download_link": "点击此处下载此功能({})", + "menu.features.download_tooltip": "从模组页面下载功能。", + "menu.features.enable_to_access_config": "启用在上述功能以访问其配置选项。", + "menu.features.enabled": "启用", + "menu.features.error_header": "错误", + "menu.features.feature_issues": "功能问题", + "menu.features.features": "功能", + "menu.features.general": "通用", + "menu.features.home": "主页", + "menu.features.no_settings_available": "此功能没有可用设置。", + "menu.features.ok_button": "确定", + "menu.features.pause_weather_overrides": "暂停天气覆盖", + "menu.features.pause_weather_tooltip": "临时禁用此功能的基于天气的设置调整。\n此状态不会被保存。", + "menu.features.profiling": "性能分析", + "menu.features.restore_defaults_tooltip": "恢复此功能的默认设置", + "menu.features.restore_override_tooltip": "从模组文件恢复原始覆盖设置。\n这将丢弃您的自定义设置并恢复为模组作者的推荐设置。", + "menu.features.scene_specific_settings": "场景特定设置", + "menu.features.select_feature_left": "请从左侧选择一个功能。", + "menu.features.select_item_left": "请从左侧选择一个项目。", + "menu.features.settings_adjusted_warning": "由于功能不兼容,您的部分设置已被自动调整。", + "menu.features.settings_hidden_disabled": "功能设置已隐藏,因为此功能在启动时被禁用。", + "menu.features.unloaded_features": "已卸载的功能", + "menu.footer.d3d12_swap_chain": "D3D12 交换链:{status}", + "menu.footer.game_version": "游戏版本:{runtime} {version}", + "menu.footer.gpu": "GPU:{name}", + "menu.home.active_constraints": "活跃设置约束", + "menu.home.click_to_navigate": "点击导航到{feature}", + "menu.home.consider_disabling_at_boot": "考虑在启动时禁用。", + "menu.home.constraint_header_constrained_by": "受限于", + "menu.home.constraint_header_forced_to": "强制为", + "menu.home.constraint_header_setting": "设置", + "menu.home.constraints_desc": "某些设置受其他功能约束。悬停在行上查看详情。", + "menu.home.dev_wiki": "开发者Wiki", + "menu.home.github": "GitHub", + "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", + "menu.home.join_discord": "加入我们的Discord", + "menu.home.nexus_mods": "Nexus Mods", + "menu.home.quick_links": "快速链接", + "menu.home.welcome": "欢迎使用Community Shaders {version}", + "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", + "menu.home.wiki": "Wiki", + "menu.issues.all_ini_loading": "所有功能INI文件加载成功。", + "menu.issues.cancel": "取消", + "menu.issues.cannot_be_undone": "此操作无法撤销!", + "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", + "menu.issues.cleanup_actions": "清理操作:", + "menu.issues.clear_issue_list": "清除问题列表", + "menu.issues.clear_issue_list_tooltip": "清除此问题列表(清理后有用)。", + "menu.issues.compilation_breaking_desc": "以下功能修改了核心着色器文件,必须通过模组管理器完全卸载。如果核心着色器被修改,仅删除INI文件不会修复编译错误。", + "menu.issues.compilation_breaking_header": "破坏编译的功能", + "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", + "menu.issues.core_feature_installed": "核心功能已安装", + "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", + "menu.issues.current_version": "当前版本:%s", + "menu.issues.delete": "删除", + "menu.issues.delete_confirm": "确定要删除功能'%s'的所有文件吗?", + "menu.issues.delete_files_tooltip": "删除与此功能关联的所有文件(INI、着色器等)", + "menu.issues.delete_unknown_tooltip": "删除此未知功能的文件。警告:如果此功能修改了核心着色器,删除可能无法修复编译问题。", + "menu.issues.download_tooltip": "下载 {name}", + "menu.issues.download_version_tooltip": "下载 {name} {version} 或更高版本", + "menu.issues.file_label": "文件:%s", + "menu.issues.files_label": "文件:", + "menu.issues.general_actions": "常规操作:", + "menu.issues.guidance_label": "指导:%s", + "menu.issues.hlsl_files_count": "%zu个HLSL文件", + "menu.issues.hlsl_files_found": "HLSL文件:找到%zu个", + "menu.issues.ini_file_label": "INI文件:%s", + "menu.issues.ini_label": "INI:%s", + "menu.issues.ini_path": "INI路径:%s", + "menu.issues.issue_label": "问题:%s", + "menu.issues.last_modified": "最后修改:", + "menu.issues.minimum_required": "最低要求:%s", + "menu.issues.no_issues": "未发现功能问题!", + "menu.issues.obsolete_compilation_failure": "此过时功能修改了核心着色器文件并导致编译失败。必须通过模组管理器卸载。", + "menu.issues.obsolete_features_desc": "以下功能已过时并已自动禁用。这些功能在此CS版本中已被移除或替换,但未修改核心着色器。", + "menu.issues.obsolete_features_header": "过时功能", + "menu.issues.open_features_folder": "打开功能文件夹", + "menu.issues.open_features_folder_tooltip": "打开包含INI文件的功能文件夹以供手动审查。", + "menu.issues.open_logs": "打开日志", + "menu.issues.open_logs_tooltip": "打开CommunityShaders.log文件以供手动审查。", + "menu.issues.open_shaders_directory": "打开着色器目录", + "menu.issues.open_shaders_tooltip": "打开主着色器目录以查看各个功能着色器文件夹。", + "menu.issues.override_failures_desc": "以下覆盖文件加载或应用失败。请检查文件格式和内容。", + "menu.issues.override_failures_header": "覆盖失败", + "menu.issues.potential_compilation_failure": "潜在的编译失败", + "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", + "menu.issues.replaced_by_prefix": "(被替换为", + "menu.issues.replaced_by_suffix": ")", + "menu.issues.replacement_label": "替代:%s", + "menu.issues.shader_directory_label": "着色器目录:%s", + "menu.issues.shader_folder": "着色器文件夹:%s", + "menu.issues.test.active_inis_count": "活动的测试 INI 文件({count}):\n", + "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", + "menu.issues.test.create_test_inis": "创建测试INI", + "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", + "menu.issues.test.feature_issue_testing": "功能问题测试", + "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", + "menu.issues.test.modified_notice": "\n部分测试文件已修改 - 建议恢复以清理", + "menu.issues.test.no_active_inis": "当前没有活跃的测试INI文件。", + "menu.issues.test.restore": "恢复", + "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", + "menu.issues.test.testing_header": "测试", + "menu.issues.this_will_delete": "这将删除:", + "menu.issues.time_label": "时间:%s", + "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", + "menu.issues.unknown_compilation_warning": "此未知功能可能修改了核心着色器文件,并可能导致编译失败。如果故障继续,应移除未知功能。", + "menu.issues.unknown_delete_warning": "这是一个未知功能。如果它修改了核心着色器文件(在其自身文件夹之外),仅删除这些文件不会修复着色器编译问题。", + "menu.issues.unknown_features_desc": "以下功能未被识别,我们已尝试自动禁用。它们可能来自开发分支或较新的CS版本。由于我们无法确定它们可能修改了哪些文件,应作为预防措施将其移除,以防止潜在的着色器编译失败。", + "menu.issues.unknown_features_header": "未知功能", + "menu.issues.update_no_link_tooltip": "此功能需要更新,但没有可用的下载链接。请手动检查模组页面。", + "menu.issues.update_required": "需要更新", + "menu.issues.update_to_version_required": "需要更新到 {version}+", + "menu.issues.use_clear_issue_list": "手动清理后使用\"清除问题列表\"刷新", + "menu.issues.use_open_features_folder": "使用\"打开功能文件夹\"手动审查INI文件", + "menu.issues.use_open_logs": "使用\"打开日志\"手动审查日志", + "menu.issues.use_open_shaders_directory": "使用\"打开着色器目录\"检查孤立着色器文件夹", + "menu.issues.warning_label": "警告:", + "menu.issues.wrong_version_desc": "以下功能存在版本兼容性问题,已自动禁用。请检查是否有更新或者该功能是否被视为过时。", + "menu.issues.wrong_version_header": "错误版本功能", + "menu.restore_settings": "恢复已保存的设置", + "menu.save_settings": "保存设置", + "menu.settings.auto_hide_feature_list": "自动隐藏功能列表", + "menu.settings.auto_hide_feature_list_tooltip": "自动隐藏左侧功能列表面板。将光标移到左边缘即可显示。", + "menu.settings.background_blur": "背景模糊", + "menu.settings.background_blur_tooltip": "对菜单窗口后面的背景应用模糊效果。", + "menu.settings.base_font_size": "基础字体大小", + "menu.settings.borders_and_separators": "边框和分隔符", + "menu.settings.button_text_align": "按钮文本对齐", + "menu.settings.button_text_align_tooltip": "当按钮大于其文本内容时应用对齐。", + "menu.settings.cancel": "取消", + "menu.settings.cell_padding": "单元格内边距", + "menu.settings.center_header_title": "居中标题", + "menu.settings.center_header_title_tooltip": "将Community Shaders标题和徽标在标题栏中居中", + "menu.settings.child_border_size": "子窗口边框大小", + "menu.settings.child_rounding": "子窗口圆角", + "menu.settings.color_background": "背景", + "menu.settings.color_border": "边框", + "menu.settings.color_border_shadow": "边框阴影", + "menu.settings.color_button": "按钮", + "menu.settings.color_button_active": "按钮(激活)", + "menu.settings.color_button_hovered": "按钮(悬停)", + "menu.settings.color_button_left": "左侧", + "menu.settings.color_button_position": "颜色按钮位置", + "menu.settings.color_button_right": "右侧", + "menu.settings.color_check_mark": "复选框勾选标记", + "menu.settings.color_child_bg": "子窗口背景", + "menu.settings.color_current_hotkey": "当前热键", + "menu.settings.color_default": "默认", + "menu.settings.color_disabled": "禁用", + "menu.settings.color_docking_empty_bg": "停靠空白背景", + "menu.settings.color_docking_preview": "停靠预览", + "menu.settings.color_drag_drop_target": "拖放目标", + "menu.settings.color_drag_drop_target_bg": "拖放目标背景", + "menu.settings.color_error": "错误", + "menu.settings.color_frame_bg": "框架背景", + "menu.settings.color_frame_bg_active": "框架背景(激活)", + "menu.settings.color_frame_bg_hovered": "框架背景(悬停)", + "menu.settings.color_header": "标题", + "menu.settings.color_header_active": "标题(激活)", + "menu.settings.color_header_hovered": "标题(悬停)", + "menu.settings.color_hovered": "悬停", + "menu.settings.color_info": "信息", + "menu.settings.color_input_text_cursor": "输入文本光标", + "menu.settings.color_menu_bar_bg": "菜单栏背景", + "menu.settings.color_minimized_transparency": "最小化透明度", + "menu.settings.color_modal_window_dim_bg": "模态窗模糊背景", + "menu.settings.color_nav_cursor": "导航光标", + "menu.settings.color_nav_windowing_dim_bg": "窗口导航模糊背景", + "menu.settings.color_nav_windowing_highlight": "窗口导航高亮", + "menu.settings.color_plot_histogram": "图表直方图", + "menu.settings.color_plot_histogram_hovered": "图表直方图(悬停)", + "menu.settings.color_plot_lines": "图表折线", + "menu.settings.color_plot_lines_hovered": "图表折线(悬停)", + "menu.settings.color_popup_bg": "弹出窗口背景", + "menu.settings.color_resize_grip": "调整大小手柄", + "menu.settings.color_resize_grip_active": "调整大小手柄(激活)", + "menu.settings.color_resize_grip_hovered": "调整大小手柄(悬停)", + "menu.settings.color_restart_needed": "需要重启", + "menu.settings.color_scrollbar_bg": "滚动条背景", + "menu.settings.color_scrollbar_grab": "滚动条滑块", + "menu.settings.color_scrollbar_grab_active": "滚动条滑块(激活)", + "menu.settings.color_scrollbar_grab_hovered": "滚动条滑块(悬停)", + "menu.settings.color_separator": "分隔符", + "menu.settings.color_separator_active": "分隔符(激活)", + "menu.settings.color_separator_hovered": "分隔符(悬停)", + "menu.settings.color_separator_line": "分隔线", + "menu.settings.color_slider_grab": "滑块手柄", + "menu.settings.color_slider_grab_active": "滑块手柄(激活)", + "menu.settings.color_slider_input_bg": "滑块和输入框背景", + "menu.settings.color_success": "成功", + "menu.settings.color_tab": "标签页", + "menu.settings.color_tab_dimmed": "标签页(变暗)", + "menu.settings.color_tab_dimmed_selected": "标签页(变暗选中)", + "menu.settings.color_tab_dimmed_selected_overline": "标签页变暗选中上划线", + "menu.settings.color_tab_hovered": "标签页(悬停)", + "menu.settings.color_tab_selected": "标签页(选中)", + "menu.settings.color_tab_selected_overline": "标签页选中上划线", + "menu.settings.color_table_border_light": "表格边框(浅色)", + "menu.settings.color_table_border_strong": "表格边框(深色)", + "menu.settings.color_table_header_bg": "表格标题背景", + "menu.settings.color_table_row_bg": "表格行背景", + "menu.settings.color_table_row_bg_alt": "表格行背景(交替)", + "menu.settings.color_text": "文本", + "menu.settings.color_text_disabled": "文本(禁用)", + "menu.settings.color_text_link": "文本链接", + "menu.settings.color_text_selected_bg": "文本选择背景", + "menu.settings.color_title_bg": "标题栏背景", + "menu.settings.color_title_bg_active": "标题栏背景(激活)", + "menu.settings.color_title_bg_collapsed": "标题栏背景(折叠)", + "menu.settings.color_tree_lines": "树状线条", + "menu.settings.color_unsaved_marker": "未保存标记", + "menu.settings.color_warning": "警告", + "menu.settings.color_window_bg": "窗口背景", + "menu.settings.color_window_border": "窗口边框", + "menu.settings.create_new_theme": "创建新主题", + "menu.settings.create_new_theme_hint": "使用当前设置创建新主题:", + "menu.settings.create_theme": "创建主题", + "menu.settings.delete_button": "删除", + "menu.settings.delete_theme": "删除主题", + "menu.settings.delete_theme_confirm_part1": "您确定要删除主题'", + "menu.settings.delete_theme_confirm_part2": "'?\n\n这将永久删除主题文件。此操作不可撤销。", + "menu.settings.delete_theme_title": "删除主题", + "menu.settings.delete_theme_tooltip": "删除'%s'的主题文件。此操作不可撤销。", + "menu.settings.description": "描述", + "menu.settings.description_tooltip": "主题的可选描述", + "menu.settings.display_name": "显示名称", + "menu.settings.display_name_duplicate": "已存在具有此显示名称的主题", + "menu.settings.display_name_tooltip": "在下拉菜单中显示的人类可读名称", + "menu.settings.docking_splitter_size": "停靠分隔器大小", + "menu.settings.effect_toggle_key": "效果切换键:", + "menu.settings.effective_size": "有效大小:%.0f px", + "menu.settings.enable_async": "启用异步", + "menu.settings.enable_async_tooltip": "如果着色器尚未编译则跳过替换。还会使编译速度极快!", + "menu.settings.enable_disk_cache": "启用磁盘缓存", + "menu.settings.enable_disk_cache_tooltip": "禁用从磁盘加载着色器,并阻止将已编译着色器保存到磁盘缓存。", + "menu.settings.feature_header_scale": "功能标题缩放", + "menu.settings.feature_header_scale_tooltip": "设置选项卡中功能标题文本的缩放倍率。", + "menu.settings.feature_headings": "功能标题", + "menu.settings.file_label": "文件:%s", + "menu.settings.filter_colors": "过滤颜色", + "menu.settings.font": "字体", + "menu.settings.font_roles": "字体角色", + "menu.settings.frame_border_size": "框架边框大小", + "menu.settings.frame_padding": "框架内边距", + "menu.settings.frame_rounding": "框架圆角", + "menu.settings.full_palette": "完整调色板", + "menu.settings.full_palette_tooltip": "用于详细自定义所有UI元素的高级颜色控制。", + "menu.settings.global_scale": "全局缩放", + "menu.settings.grab_min_size": "滑块最小大小", + "menu.settings.grab_rounding": "滑块圆角", + "menu.settings.indent_spacing": "缩进间距", + "menu.settings.item_inner_spacing": "项目内部间距", + "menu.settings.item_spacing": "项目间距", + "menu.settings.language": "语言", + "menu.settings.language_tooltip": "选择Community Shaders界面的显示语言。", + "menu.settings.last_shader_cache_duration": "上次着色器缓存构建持续时间:%s", + "menu.settings.log_slider_deadzone": "对数滑块死区", + "menu.settings.no_families": "无字体家族", + "menu.settings.no_font_families_available": "无可用字体家族", + "menu.settings.no_fonts_found": "未找到字体。请将.ttf文件放入Interface/CommunityShaders/Fonts/", + "menu.settings.no_style_variants": "未找到此字体家族的样式变体。", + "menu.settings.no_styles": "无样式", + "menu.settings.open_themes_folder": "打开主题文件夹", + "menu.settings.open_themes_folder_tooltip": "打开主题文件夹,您可以在其中添加自定义主题文件。", + "menu.settings.overlay_toggle_key": "叠加层切换键:", + "menu.settings.popup_border_size": "弹出窗口边框大小", + "menu.settings.popup_rounding": "弹出窗口圆角", + "menu.settings.refresh": "刷新", + "menu.settings.refresh_font_families": "刷新字体家族", + "menu.settings.refresh_font_families_tooltip": "添加或删除字体文件后重新扫描字体目录。", + "menu.settings.require_shift_to_dock": "需要Shift键停靠", + "menu.settings.require_shift_to_dock_tooltip": "启用时,拖动时必须按住Shift键才能停靠/对齐窗口。防止意外停靠。", + "menu.settings.reset": "重置", + "menu.settings.save_as_new_theme": "保存为新主题", + "menu.settings.save_theme_button": "保存", + "menu.settings.save_theme_tooltip": "使用当前设置更新当前选中的主题(%s)", + "menu.settings.screenshot_key": "截图键:", + "menu.settings.scrollbar_opacity": "滚动条不透明度", + "menu.settings.scrollbar_rounding": "滚动条圆角", + "menu.settings.scrollbar_size": "滚动条大小", + "menu.settings.section_borders": "边框", + "menu.settings.section_docking": "停靠", + "menu.settings.section_language": "语言", + "menu.settings.section_layout": "布局", + "menu.settings.section_main": "主页", + "menu.settings.section_rounding": "圆角", + "menu.settings.section_tables": "表格", + "menu.settings.section_widgets": "控件", + "menu.settings.selectable_text_align": "可选取文本对齐", + "menu.settings.selectable_text_align_tooltip": "当可选取项大于其文本内容时应用对齐。", + "menu.settings.selected_theme": "已选主题:", + "menu.settings.separator_text_align": "分隔符文本对齐", + "menu.settings.separator_text_border_size": "分隔符文本边框大小", + "menu.settings.separator_text_padding": "分隔符文本内边距", + "menu.settings.shader_deduplicated": "已去重", + "menu.settings.shader_disk_cache": "磁盘缓存", + "menu.settings.shader_failed": "失败", + "menu.settings.shader_fast": "快速(<2秒)", + "menu.settings.shader_slow": "慢速(2-8秒)", + "menu.settings.shader_very_slow": "非常慢(>=8秒)", + "menu.settings.show_footer": "显示页脚", + "menu.settings.show_footer_tooltip": "在窗口底部显示包含游戏版本、交换链和GPU信息的页脚", + "menu.settings.show_icon_buttons_in_header": "在标题栏中显示图标按钮", + "menu.settings.show_icon_buttons_in_header_tooltip": "启用时:在标题栏中将操作按钮(保存、加载、清除缓存)显示为图标\n禁用时:在标题栏下方显示为文本按钮", + "menu.settings.skip_clear_cache_dialogue": "跳过清除缓存对话框", + "menu.settings.skip_clear_cache_dialogue_tooltip": "勾选时,着色器缓存将立即清除,无需确认。", + "menu.settings.skip_compilation_key": "跳过编译键:", + "menu.settings.skip_unchanged_shaders": "跳过未更改的着色器", + "menu.settings.skip_unchanged_shaders_tooltip": "启用时,仅当每个着色器的.hlsl文件比磁盘上缓存的.bin更新时才从源代码重新编译。源文件未更改的着色器直接从磁盘缓存加载,避免完整的启动编译开销。适用于迭代测试:更改着色器文件后仅重建该着色器。需要\"启用磁盘缓存\"处于活动状态。", + "menu.settings.status": "状态", + "menu.settings.tab_bar_border_size": "标签栏边框大小", + "menu.settings.tab_behavior": "行为", + "menu.settings.tab_border_size": "标签页边框大小", + "menu.settings.tab_colors": "颜色", + "menu.settings.tab_fonts": "字体", + "menu.settings.tab_interface": "界面", + "menu.settings.tab_keybindings": "按键绑定", + "menu.settings.tab_rounding": "标签页圆角", + "menu.settings.tab_shaders": "着色器", + "menu.settings.tab_styling": "样式", + "menu.settings.tab_themes": "主题", + "menu.settings.table_angled_headers_angle": "表格斜角标题角度", + "menu.settings.theme_name": "主题名称", + "menu.settings.theme_name_duplicate": "已存在具有此名称的主题", + "menu.settings.theme_name_required": "主题名称为必填", + "menu.settings.theme_name_tooltip": "主题的文件名(不含.json扩展名)", + "menu.settings.theme_preset": "主题预设", + "menu.settings.theme_save_info": "主题更改不会随全局\"保存设置\"按钮保存。使用主题选项卡将更改保存到此主题。", + "menu.settings.theme_save_reminder": "如果您更改了上述主题,请使用全局\"保存设置\"按钮保存您的选择。", + "menu.settings.theme_update_failed": "更新主题失败", + "menu.settings.theme_updated_no_changes": "主题更新成功 - 未检测到更改", + "menu.settings.theme_updated_with_changes": "主题更新成功!更改的设置:", + "menu.settings.thumb_active_opacity": "滑块激活不透明度", + "menu.settings.thumb_active_opacity_tooltip": "控制滚动条滑块被拖动时的不透明度。", + "menu.settings.thumb_hovered_opacity": "滑块悬停不透明度", + "menu.settings.thumb_hovered_opacity_tooltip": "控制滚动条滑块悬停时的不透明度。", + "menu.settings.thumb_opacity": "滑块不透明度", + "menu.settings.thumb_opacity_tooltip": "控制滚动条滑块(可拖动的部分)的不透明度。", + "menu.settings.toggle_key": "切换键:", + "menu.settings.tooltip_hover_delay": "工具提示悬停延迟", + "menu.settings.tooltip_hover_delay_tooltip": "悬停在项目上时工具提示出现前等待的秒数。", + "menu.settings.track_opacity": "滚动轨道不透明度", + "menu.settings.track_opacity_tooltip": "控制滚动条轨道/通道(滚动条后面的背景区域)的不透明度。", + "menu.settings.ui_behavior": "UI行为", + "menu.settings.use_custom_shaders": "使用自定义着色器", + "menu.settings.use_custom_shaders_tooltip": "禁用此项实际上会禁用所有功能。", + "menu.settings.use_monochrome_cs_logo": "使用单色CS徽标", + "menu.settings.use_monochrome_cs_logo_tooltip": "使用Community Shaders徽标的单色版本", + "menu.settings.use_monochrome_icons": "使用单色图标", + "menu.settings.use_monochrome_icons_tooltip": "使用适应主题文本颜色的白色单色图标", + "menu.settings.use_resolution_based_font_size": "使用基于分辨率的字体大小", + "menu.settings.use_resolution_based_font_size_tooltip": "启用时,UI字体大小根据屏幕分辨率缩放。禁用以设置固定大小。", + "menu.settings.visual_effects": "视觉效果", + "menu.settings.window_border_size": "窗口边框大小", + "menu.settings.window_padding": "窗口内边距", + "menu.settings.window_rounding": "窗口圆角", + "menu.setup.change_later": "您可以稍后在通用 > 按键绑定中更改此项。", + "menu.setup.choose_hotkey": "请选择一个热键来访问菜单:", + "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", + "menu.setup.new_install_line2": "重新安装Community Shaders。", + "menu.setup.press_any_key": "按下任意键设置为切换键...", + "menu.setup.press_to_close": "按Escape或Enter继续", + "menu.toggle_error_message": "切换错误消息", + "menu.toggle_error_message_tooltip": "隐藏或显示着色器失败消息。您的安装已损坏,游戏中可能会看到错误。请仔细检查是否已更新所有功能以及加载顺序是否正确。请参阅CommunityShaders.log了解详情,并查看Nexus Mods页面或Discord服务器。", + "menu.window_title": "Community Shaders {version}", + "menu.window_title_dev": "Community Shaders {version} [{build}]", + "overlay.modified_features": "检测到可能修改了着色器的功能。请检查菜单中的功能问题。", + "overlay.shader_blocking_active": "着色器拦截已激活", + "overlay.uncompiled_warning": "警告:未编译的着色器在加载时会有视觉错误或导致卡顿。", + "ui.cancel": "取消", + "ui.clear_cache": "清除缓存", + "ui.clear_cache_confirm": "您确定要清除着色器缓存吗?", + "ui.clear_cache_desc": "这将清除内存和磁盘缓存(如果启用)中的所有已编译着色器。着色器将在游戏下次遇到它们时重新编译。", + "ui.clear_shader_cache": "清除着色器缓存?", + "ui.copy": "复制", + "ui.dont_ask_again": "不再提示", + "ui.search": "搜索...", + "ui.search_features": "搜索功能...", + "feature.cs_editor.description": "用于在游戏内检查、编辑和预览面向渲染器数据的开发工具。", + "feature.cs_editor.key_feature_1": "提供天气编辑功能", + "feature.cs_editor.key_feature_2": "包含对原版后处理和天气设置的动态保存与加载。", + "feature.cs_editor.key_feature_3": "实时编辑和预览效果", + "feature.cs_editor.key_feature_4": "即时切换任意天气,支持立即或渐变过渡", + "feature.cs_editor.key_feature_5": "按类型筛选天气(晴朗、多云、雨天、雪天、极光),方便浏览", + "feature.cs_editor.key_feature_6": "查看详细的天气信息,包括风、降水和闪电数据", + "feature.cs_editor.key_feature_7": "颜色编码的天气名称,一目了然地展示所有天气属性", + "feature.cs_editor.key_feature_8": "持久叠加窗口,可在游戏过程中持续监控天气", + "feature.cs_editor.name": "CS 编辑器", + "menu.settings.cs_editor_toggle_key": "CS 编辑器切换键:", + "menu.setup.cs_editor_unbound": "CS 编辑器热键未绑定 - 所选键使用 Shift", + "menu.setup.cs_editor_will_be": "CS 编辑器热键将为:{key}", + "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "原版镜面反射贴图倍率,应用到第一层粗糙度。", + "feature.skin.adds_a_constant_layer_of_wetness_to_all": "为所有皮肤添加一层恒定湿润度,使其始终略显潮湿或出汗,即使角色不在水中或没有剧烈活动。", + "feature.skin.advanced_skin_shader_using_dual_specular_lobes": "使用双镜面反射叶瓣的高级皮肤着色器。", + "feature.skin.base_color_multiplier": "基础色倍率", + "feature.skin.body_tiling_multiplier": "身体平铺倍率", + "feature.skin.controls_how_bumpy_wet_skin_appears_higher_values": "控制湿皮肤看起来有多凹凸。较高的值会让湿润区域出现更明显的表面波纹和扭曲。", + "feature.skin.controls_how_much_fine_detail_is_added_to": "控制添加到湿润图案中的细节量。较高的值会在基础图案上叠加更多小尺度变化。", + "feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer": "控制角质层的微观粗糙度。", + "feature.skin.controls_the_overall_contrast_and_roughness_of_the": "控制湿润图案的整体对比度和粗糙度。较高的值会让图案更明显、变化更多。", + "feature.skin.controls_the_size_of_the_wet_dry_pattern": "控制皮肤上干湿图案的大小。较高的值会产生更细、更详细的图案;较低的值会产生更大、更宽的湿斑。", + "feature.skin.description": "高级皮肤通过多种技术增强角色皮肤渲染。", + "feature.skin.dynamic_wetness_detected": "检测到动态湿润度。", + "feature.skin.enable_advanced_skin": "启用高级皮肤", + "feature.skin.enable_skin_detail": "启用皮肤细节", + "feature.skin.enable_skin_detail_texture": "启用皮肤细节纹理", + "feature.skin.enable_sss_transmission": "启用 SSS 透射", + "feature.skin.extra_edge_roughness": "额外边缘粗糙度", + "feature.skin.extra_roughness_at_the_edges_of_the_skin": "皮肤边缘的额外粗糙度,用于近似脸部细绒毛。", + "feature.skin.extra_skin_wetness": "额外皮肤湿润度", + "feature.skin.fresnel_f0": "菲涅尔 F0", + "feature.skin.fresnel_reflectance": "菲涅尔反射率", + "feature.skin.full_sweat_threshold": "满汗阈值", + "feature.skin.fuzz_f0": "细绒毛 F0", + "feature.skin.fuzz_roughness": "细绒毛粗糙度", + "feature.skin.fuzz_strength": "细绒毛强度", + "feature.skin.how_many_seconds_it_takes_for_skin_to": "离开水后皮肤完全变干所需的秒数。较高的值会让湿润持续更久。", + "feature.skin.intensity_of_secondary_specular_highlights": "次级镜面反射高光强度。", + "feature.skin.key_feature_1": "基于物理的双镜面反射叶瓣,提供更真实的皮肤高光", + "feature.skin.key_feature_2": "平铺皮肤细节纹理,提升真实感", + "feature.skin.key_feature_3": "支持额外的粗糙度、半透明和湿润度纹理", + "feature.skin.key_feature_4": "重做湿润系统,用于动态皮肤效果", + "feature.skin.multiplier_for_specular_map": "镜面反射贴图倍率", + "feature.skin.multiplier_for_the_base_color_texture": "基础色纹理倍率。", + "feature.skin.multiply_the_tiling_for_the_body_to_match": "将身体平铺倍率乘上该值以匹配脸部。", + "feature.skin.name": "高级皮肤", + "feature.skin.options_for_additional_roughness_and_specular_maps": "额外粗糙度和镜面反射贴图选项。", + "feature.skin.physical_main_roughness_multiplier": "物理主粗糙度倍率", + "feature.skin.physical_second_roughness_multiplier": "物理次粗糙度倍率", + "feature.skin.physical_specular_multiplier": "物理镜面反射倍率", + "feature.skin.primary_roughness": "主粗糙度", + "feature.skin.reload_skin_detail_texture": "重新加载皮肤细节纹理", + "feature.skin.secondary_roughness": "次粗糙度", + "feature.skin.secondary_specular_strength": "次级镜面反射强度", + "feature.skin.should_be_30_50_lower_than_primary": "应比主粗糙度低 30-50%%。", + "feature.skin.skin_detail_strength": "皮肤细节强度", + "feature.skin.skin_detail_tiling": "皮肤细节平铺", + "feature.skin.smoothness_of_epidermal_cell_layer_reflections": "表皮细胞层反射的平滑度。", + "feature.skin.specular_texture_multiplier": "镜面反射纹理倍率", + "feature.skin.sss_width": "SSS 宽度", + "feature.skin.stamina_threshold_for_sweat": "出汗耐力阈值", + "feature.skin.strength_of_skin_detail_texture": "皮肤细节纹理强度。", + "feature.skin.the_character_reaches_maximum_sweat_when_stamina_drops": "当耐力低于此百分比时,角色达到最大出汗量。例如 0.15 表示耐力低于 15%% 时满汗。", + "feature.skin.the_character_starts_sweating_when_their_stamina_drops": "当耐力低于此百分比时,角色开始出汗。例如 0.75 表示耐力低于 75%% 时出现汗水。", + "feature.skin.the_more_tiling_the_more_detailed_the_skin": "平铺越多,皮肤细节越丰富。", + "feature.skin.translucency": "半透明度", + "feature.skin.translucency_of_the_sss_transmittance_effect": "SSS 透射效果的半透明度。", + "feature.skin.use_dynamic_wetness": "使用动态湿润度", + "feature.skin.wetness_fade_out_time": "湿润度淡出时间", + "feature.skin.wetness_normal_scale": "湿润法线强度", + "feature.skin.wetness_perlin_noise_lacunarity": "湿润 Perlin 噪声频率倍增", + "feature.skin.wetness_perlin_noise_persistence": "湿润 Perlin 噪声持续度", + "feature.skin.wetness_perlin_noise_scale": "湿润 Perlin 噪声比例", + "feature.skin.width_of_the_sss_transmittance_effect": "SSS 透射效果的宽度。", + "cs_editor.actions": "操作", + "cs_editor.active": "活跃:", + "cs_editor.active_click_pause": "活跃 - 点击暂停", + "cs_editor.add": "添加", + "cs_editor.add_new_marker": "添加新标记", + "cs_editor.ambient_color": "环境光颜色", + "cs_editor.ambient_directional": "环境和方向光", + "cs_editor.apply": "应用", + "cs_editor.apply_changes": "将更改应用到游戏", + "cs_editor.art_object": "艺术对象", + "cs_editor.attach_to_camera": "附着到摄像机", + "cs_editor.auto_apply_changes": "自动应用更改", + "cs_editor.auto_apply_changes_tooltip": "编辑时自动将天气更改应用于游戏", + "cs_editor.box_size": "包围盒尺寸", + "cs_editor.cancel": "取消", + "cs_editor.categories": "类别", + "cs_editor.category_cell_lighting": "单元格光照", + "cs_editor.category_imagespace": "图像空间", + "cs_editor.category_interior_only": "仅室内", + "cs_editor.category_lens_flare": "镜头光晕", + "cs_editor.category_lighting_editor": "光照编辑器", + "cs_editor.category_lighting_template": "光照模板", + "cs_editor.category_lightning": "闪电", + "cs_editor.category_precipitation": "降水", + "cs_editor.category_shader_particle": "着色器粒子几何体", + "cs_editor.category_sun": "太阳", + "cs_editor.category_visual_effect": "视觉效果", + "cs_editor.category_visual_effects": "视觉效果", + "cs_editor.category_volumetric_lighting": "体积光照", + "cs_editor.category_weather": "天气", + "cs_editor.category_weather_transition": "天气转换", + "cs_editor.category_wind": "风", + "cs_editor.cell_lighting_interior_only": "单元格光照仅适用于室内单元格。", + "cs_editor.center_offset_max": "中心偏移最大值", + "cs_editor.center_offset_min": "中心偏移最小值", + "cs_editor.changes_require_manual_apply": "(更改需要手动应用)", + "cs_editor.clear_favorites": "清除收藏", + "cs_editor.clear_favourite": "清除收藏", + "cs_editor.clear_recent_history": "清除最近历史", + "cs_editor.click_plus_add": "点击 + 添加仅在室内应用的设置。", + "cs_editor.click_to_copy": "点击复制", + "cs_editor.clip_distance": "裁剪距离", + "cs_editor.close_all_widgets": "关闭所有 {} 控件", + "cs_editor.cloud_alpha": "云透明度", + "cs_editor.cloud_color": "云颜色", + "cs_editor.cloud_layer": "云层 {}", + "cs_editor.cloud_layer_speed_x": "云层速度 X", + "cs_editor.cloud_layer_speed_y": "云层速度 Y", + "cs_editor.color": "颜色", + "cs_editor.color_ambient": "环境光", + "cs_editor.color_cloud_lod_ambient": "云LOD环境光", + "cs_editor.color_cloud_lod_diffuse": "云LOD漫反射", + "cs_editor.color_effect_lighting": "效果光照", + "cs_editor.color_fog_far": "远雾颜色", + "cs_editor.color_fog_near": "近雾颜色", + "cs_editor.color_horizon": "地平线", + "cs_editor.color_moon_glare": "月亮眩光", + "cs_editor.color_sky_lower": "天空下部", + "cs_editor.color_sky_statics": "天空静态", + "cs_editor.color_sky_upper": "天空上部", + "cs_editor.color_stars": "星星", + "cs_editor.color_sun": "太阳", + "cs_editor.color_sun_glare": "太阳眩光", + "cs_editor.color_sunlight": "阳光", + "cs_editor.color_water_multiplier": "水面倍率", + "cs_editor.colors_3_plus": "(使用3次以上的颜色将显示在此处)", + "cs_editor.colour": "颜色", + "cs_editor.colours": "颜色", + "cs_editor.confirm_delete_saved_file": "您确定要删除已保存的设置文件吗?", + "cs_editor.contribution": "贡献度", + "cs_editor.copy_all_from_parent": "从父级天气复制所有参数值", + "cs_editor.currently_exterior_cell": "您当前处于室外单元格。", + "cs_editor.custom_color": "自定义颜色", + "cs_editor.custom_color_contribution": "自定义颜色贡献度", + "cs_editor.custom_overrides_tooltip_0": "此天气对该功能有自定义覆写。", + "cs_editor.custom_overrides_tooltip_1": "点击禁用覆写,改用全局设置。", + "cs_editor.custom_overrides_tooltip_2": "(设置将被保留但不会应用)", + "cs_editor.dalc_directional_x_max": "方向光 +X", + "cs_editor.dalc_directional_x_min": "方向光 -X", + "cs_editor.dalc_directional_y_max": "方向光 +Y", + "cs_editor.dalc_directional_y_min": "方向光 -Y", + "cs_editor.dalc_directional_z_max": "方向光 +Z", + "cs_editor.dalc_directional_z_min": "方向光 -Z", + "cs_editor.dalc_fresnel_power": "菲涅尔强度", + "cs_editor.dalc_header": "方向环境光(DALC)", + "cs_editor.dalc_specular": "高光", + "cs_editor.day": "白天", + "cs_editor.day_far": "白天远", + "cs_editor.day_max": "白天最大值", + "cs_editor.day_near": "白天近", + "cs_editor.day_power": "白天强度", + "cs_editor.delete": "删除", + "cs_editor.delete_all": "全部删除", + "cs_editor.delete_json_file": "删除JSON文件", + "cs_editor.delete_overwrite_file": "从磁盘删除覆盖文件", + "cs_editor.delete_saved_file": "删除已保存的文件", + "cs_editor.delete_saved_file_tooltip": "删除已保存的文件", + "cs_editor.density_contribution": "密度贡献度", + "cs_editor.density_settings": "密度设置", + "cs_editor.density_size": "密度尺寸", + "cs_editor.direction_x_minus": "X-(左)", + "cs_editor.direction_x_plus": "X+(右)", + "cs_editor.direction_y_minus": "Y-(后)", + "cs_editor.direction_y_plus": "Y+(前)", + "cs_editor.direction_z_minus": "Z-(下)", + "cs_editor.direction_z_plus": "Z+(上)", + "cs_editor.directional_color": "方向光颜色", + "cs_editor.directional_colors": "方向光颜色", + "cs_editor.directional_fade": "方向光衰减", + "cs_editor.directional_settings": "方向光设置", + "cs_editor.directional_xy": "方向光 XY", + "cs_editor.directional_z": "方向光 Z", + "cs_editor.drag_colours_here": "将颜色拖动到此处以保存为收藏。", + "cs_editor.drag_to_favourites": "将颜色拖动到此处以添加到收藏", + "cs_editor.edit_current_cell_lighting": "编辑当前单元格光照", + "cs_editor.editor_flags": "编辑器标志", + "cs_editor.editor_id": "编辑器ID", + "cs_editor.editor_id_label": "编辑器ID:%s", + "cs_editor.editor_ui_scale": "编辑器UI缩放", + "cs_editor.editor_ui_scale_tooltip": "缩放所有编辑器UI元素的大小(0.5 = 50%,2.0 = 200%)", + "cs_editor.effect_shader": "效果着色器", + "cs_editor.enable": "启用", + "cs_editor.enable_inherit_feature": "启用\"从父级继承\"功能", + "cs_editor.enable_inherit_feature_tooltip": "显示复选框以从父级天气复制设置(仅编辑器功能)", + "cs_editor.enable_inherit_from_parent": "启用从父级继承", + "cs_editor.enable_inherit_tooltip": "在天气控件中显示从父级继承选项", + "cs_editor.enable_weather_overrides_hint": "启用上方天气特定覆写以自定义此天气的设置。", + "cs_editor.enabled_badge": "[已启用]", + "cs_editor.exit_free_camera": "退出自由相机", + "cs_editor.exit_play_mode": "退出游玩模式", + "cs_editor.face_target": "面向目标", + "cs_editor.falling_speed": "下落速度", + "cs_editor.fav": "收藏", + "cs_editor.fav_most_colours": "收藏/最常用颜色在此处。", + "cs_editor.fav_most_values": "收藏/最常用值在此处。", + "cs_editor.favorites": "收藏夹", + "cs_editor.favorites_count": "收藏:%d", + "cs_editor.favourites": "收藏夹", + "cs_editor.feature_specific_settings": "配置功能特定的设置,当此天气激活时应用。这些设置会针对此天气覆盖功能的全局设置。", + "cs_editor.features": "功能", + "cs_editor.file": "文件", + "cs_editor.file_label": "文件:%s", + "cs_editor.filter_all": "全部", + "cs_editor.filter_editor_id": "编辑器ID", + "cs_editor.filter_file": "文件", + "cs_editor.filter_form_id": "表单ID", + "cs_editor.filter_help": "按所选列筛选对象列表。\n全部:搜索编辑器ID、Form ID、文件和状态。\n状态:搜索框非空时隐藏无状态标记的项目。\nCtrl+F:聚焦搜索\nEnter:打开选中项", + "cs_editor.filter_hint": "筛选... (Ctrl+F)", + "cs_editor.filter_status": "状态", + "cs_editor.flagged": "已标记", + "cs_editor.flags": "标志", + "cs_editor.fog_clamp": "雾限制", + "cs_editor.fog_color_far": "远雾颜色", + "cs_editor.fog_color_near": "近雾颜色", + "cs_editor.fog_far": "远", + "cs_editor.fog_max": "最大值", + "cs_editor.fog_near": "近", + "cs_editor.fog_power": "雾强度", + "cs_editor.fog_power_short": "强度", + "cs_editor.force_this_weather": "强制使用此天气", + "cs_editor.force_weather": "强制天气", + "cs_editor.form_id": "表单ID", + "cs_editor.form_id_label": "Form ID?%08X", + "cs_editor.form_record_references": "此天气使用的表单记录引用。", + "cs_editor.form_reference_note": "此表单被天气记录引用。要更改使用的表单,请在天气控件中编辑\"记录\"选项卡。", + "cs_editor.free_camera_scroll": "自由相机(滚动调节速度)", + "cs_editor.game_time": "游戏时间", + "cs_editor.game_time_tooltip": "调整当前游戏时间", + "cs_editor.general": "通用", + "cs_editor.general_settings": "通用设置", + "cs_editor.global_settings_tooltip_0": "此天气使用全局功能设置。", + "cs_editor.global_settings_tooltip_1": "点击以启用天气特定覆写。", + "cs_editor.gravity_velocity": "重力速度", + "cs_editor.help": "帮助", + "cs_editor.imagespace_label": "图像空间:", + "cs_editor.imagespaces_count": "图像空间:%d", + "cs_editor.inherit_all": "全部继承", + "cs_editor.inherit_ambient_color": "继承环境光颜色", + "cs_editor.inherit_clip_distance": "继承裁剪距离", + "cs_editor.inherit_directional_color": "继承方向光颜色", + "cs_editor.inherit_directional_fade": "继承方向光衰减", + "cs_editor.inherit_directional_rotation": "继承方向光旋转", + "cs_editor.inherit_flags_desc": "这些标志控制从单元格的光照模板继承哪些光照属性。", + "cs_editor.inherit_fog_color": "继承雾颜色", + "cs_editor.inherit_fog_far": "继承远雾", + "cs_editor.inherit_fog_max_clamp": "继承雾最大值(限制)", + "cs_editor.inherit_fog_near": "继承近雾", + "cs_editor.inherit_fog_power": "继承雾强度", + "cs_editor.inherit_from_parent": "从父级继承", + "cs_editor.inherit_from_parent_weather": "从父级天气继承", + "cs_editor.inherit_light_fade_distances": "继承光照衰减距离", + "cs_editor.inherit_rotation": "继承旋转", + "cs_editor.inherited_from_lighting_template": "从光照模板继承", + "cs_editor.inherited_from_parent_weather": "从父级天气继承", + "cs_editor.inheriting_from_parent": "正在从父级继承", + "cs_editor.intensity": "强度", + "cs_editor.interior_cell": "室内单元格", + "cs_editor.interior_only_available": "仅在室内单元格中可用", + "cs_editor.interior_only_settings": "仅室内设置", + "cs_editor.interior_settings_override": "添加到此处的设置将在您进入室内单元格时覆盖功能默认值。退出时值会自动恢复。", + "cs_editor.json": "JSON", + "cs_editor.keyboard_shortcuts": "键盘快捷键:", + "cs_editor.label": "标签", + "cs_editor.light_fade": "光照渐隐", + "cs_editor.light_fade_end": "光照衰减结束", + "cs_editor.light_fade_start": "光照衰减开始", + "cs_editor.lighting_count": "光照:%d", + "cs_editor.lightning_color_label": "闪电颜色", + "cs_editor.load": "加载", + "cs_editor.load_saved_file": "加载已保存的文件(如果没有文件则重置为原版)", + "cs_editor.locked_weather_status": " [已锁定:%s]", + "cs_editor.manual_apply_required_tooltip": "此表单类型仅在天气重新初始化时由引擎重新读取。\n自动应用已禁用 - 请使用应用按钮。", + "cs_editor.max_recent_widgets": "最大最近控件数", + "cs_editor.max_recent_widgets_tooltip": "要记住的最大最近控件数量", + "cs_editor.menu": "菜单", + "cs_editor.more_results": "... 还有{}个结果", + "cs_editor.most_used": "最常用", + "cs_editor.night": "夜晚", + "cs_editor.night_far": "夜晚远", + "cs_editor.night_max": "夜晚最大值", + "cs_editor.night_near": "夜晚近", + "cs_editor.night_power": "夜晚强度", + "cs_editor.no_art_objects_available": "没有可用的艺术对象", + "cs_editor.no_effect_shaders_available": "没有可用的效果着色器", + "cs_editor.no_frequent_colors": "暂无常用颜色", + "cs_editor.no_frequent_values": "暂无常用值", + "cs_editor.no_interior_settings": "未配置仅室内设置。", + "cs_editor.no_lighting_data": "此单元格没有可用的光照数据。", + "cs_editor.no_open_widgets": "没有打开的控件", + "cs_editor.no_recent_colors": "没有最近使用过的颜色", + "cs_editor.no_recent_values": "没有最近使用过的值", + "cs_editor.no_widgets_open": "没有控件打开", + "cs_editor.none": "无", + "cs_editor.none_filter": "无", + "cs_editor.not_interior_cell": "此单元格不是室内单元格。", + "cs_editor.not_same_as_cell_lighting": "注意:这与单元格光照模板继承不同。", + "cs_editor.num_subtextures_x": "子纹理数量 X", + "cs_editor.num_subtextures_y": "子纹理数量 Y", + "cs_editor.objects": "对象", + "cs_editor.offset": "偏移", + "cs_editor.open": "打开", + "cs_editor.open_imagespace_edit": "打开此图像空间进行编辑", + "cs_editor.open_precipitation_edit": "打开此降水进行编辑", + "cs_editor.open_visual_effect_edit": "打开此视觉效果进行编辑", + "cs_editor.open_volumetric_edit": "打开此体积光照进行编辑", + "cs_editor.open_widgets": "打开控件:", + "cs_editor.options": "选项", + "cs_editor.other": "其他", + "cs_editor.overwrite_files": "覆盖文件", + "cs_editor.palette": "调色板", + "cs_editor.parameter": "参数", + "cs_editor.parent": "父级", + "cs_editor.particle_density_label": "粒子密度", + "cs_editor.particle_shader": "粒子着色器", + "cs_editor.particle_size": "粒子大小", + "cs_editor.particle_texture_label": "粒子纹理", + "cs_editor.particle_type": "粒子类型", + "cs_editor.path_must_end_dds": "路径必须以'.dds'结尾", + "cs_editor.pause_all": "全部暂停", + "cs_editor.pause_time": "暂停时间", + "cs_editor.pause_time_tooltip": "暂停或恢复游戏时间推进", + "cs_editor.paused_click_resume": "已暂停 - 点击恢复", + "cs_editor.phase_function": "相位函数", + "cs_editor.phase_function_contribution": "相位函数贡献度", + "cs_editor.phase_function_scattering": "相位函数散射", + "cs_editor.play_mode_walk": "游玩模式 - 正常行走", + "cs_editor.player_cell_unavailable": "玩家单元格不可用。", + "cs_editor.precipitation_begin_fade_in_label": "降水开始淡入", + "cs_editor.precipitation_end_fade_out_label": "降水结束淡出", + "cs_editor.preview_free_camera": " [ %s ] 自由摄像机(速度:%.0f)", + "cs_editor.preview_free_camera_locked": " [ %s ] 自由摄像机已锁定", + "cs_editor.preview_play_mode": " [ %s ] 播放模式", + "cs_editor.quick_tips": "快速提示:", + "cs_editor.rain": "雨", + "cs_editor.range_factor": "范围因子", + "cs_editor.recent": "最近:", + "cs_editor.recent_count": "最近:%d", + "cs_editor.recently_used": "最近使用", + "cs_editor.record_imagespace": "图像空间", + "cs_editor.record_precipitation": "降水", + "cs_editor.record_visual_effect": "视觉效果", + "cs_editor.record_volumetric_lighting": "体积光照", + "cs_editor.remove": "移除", + "cs_editor.remove_from_palette": "从调色板移除", + "cs_editor.remove_setting": "移除此设置", + "cs_editor.reset_speed": "重置速度", + "cs_editor.reset_speed_tooltip": "将时间速度重置为原版(%.1f倍)", + "cs_editor.reset_to_default": "重置为1.0", + "cs_editor.reset_to_global": "重置为全局设置", + "cs_editor.reset_ui_scale_tooltip": "将UI缩放重置为默认值(100%)", + "cs_editor.reset_window_layout": "重置窗口布局", + "cs_editor.resume_time": "恢复时间", + "cs_editor.revert": "还原", + "cs_editor.revert_to_game_values": "还原为游戏值", + "cs_editor.revert_to_original": "还原为原始游戏值", + "cs_editor.rgb_color": "RGB 颜色", + "cs_editor.right_click_to_clear": "右键点击清除", + "cs_editor.right_click_to_remove": "右键点击移除", + "cs_editor.rotation_velocity": "旋转速度", + "cs_editor.sampling": "采样", + "cs_editor.sampling_range_factor": "采样范围因子", + "cs_editor.save": "保存", + "cs_editor.save_all_open_widgets": "保存所有打开的控件", + "cs_editor.save_to_file": "保存到文件", + "cs_editor.save_widget": "保存 {}", + "cs_editor.scattering": "散射", + "cs_editor.search_settings_hint": "搜索设置(Ctrl+F)", + "cs_editor.select_feature": "选择功能...", + "cs_editor.select_setting": "选择设置...", + "cs_editor.session_history": "会话和历史", + "cs_editor.settings": "设置", + "cs_editor.shortcut_ctrl_f": "Ctrl+F:聚焦搜索", + "cs_editor.shortcut_ctrl_s": "Ctrl+S:保存所有打开的控件", + "cs_editor.shortcut_ctrl_w": "Ctrl+W:关闭焦点控件", + "cs_editor.shortcut_enter": "Enter:打开选中的控件", + "cs_editor.shortcut_esc": "Esc:关闭编辑器", + "cs_editor.size": "大小", + "cs_editor.size_x": "尺寸 X", + "cs_editor.size_y": "尺寸 Y", + "cs_editor.snow": "雪", + "cs_editor.start_rotation_range": "起始旋转范围", + "cs_editor.status": "状态", + "cs_editor.subtextures": "子纹理", + "cs_editor.sun_damage": "阳光伤害", + "cs_editor.tab_advanced": "高级", + "cs_editor.tab_basic": "基础", + "cs_editor.tab_dalc": "DALC", + "cs_editor.tab_density": "密度", + "cs_editor.tab_fog": "雾", + "cs_editor.tab_inheritance": "继承", + "cs_editor.tab_particle": "粒子", + "cs_editor.tab_position": "位置", + "cs_editor.tab_texture": "纹理", + "cs_editor.text_buttons_tooltip": "将操作按钮显示为文本标签而非图标", + "cs_editor.texture_file_not_found": "在Data/textures/下找不到纹理文件。", + "cs_editor.texture_path": "纹理路径", + "cs_editor.thunder_lightning_begin_fade_in": "雷声闪电开始淡入", + "cs_editor.thunder_lightning_end_fade_out": "雷声闪电结束淡出", + "cs_editor.thunder_lightning_frequency": "雷声闪电频率", + "cs_editor.time_paused_status": " [时间已暂停]", + "cs_editor.time_scale_tooltip": "调整时间流逝速度(原版:%.1f倍)", + "cs_editor.tip_auto_apply": "自动应用会实时更新游戏", + "cs_editor.tip_double_click": "双击编辑", + "cs_editor.tip_lock_weather": "锁定天气以防止更改", + "cs_editor.tip_quick_filters": "使用快速过滤进行快速排序", + "cs_editor.tip_right_click": "右键点击标记状态", + "cs_editor.tip_star_favorite": "点击星形图标收藏", + "cs_editor.tip_undo": "撤消按钮还原最近的更改(Ctrl+Z)", + "cs_editor.tod_day": "白天", + "cs_editor.tod_night": "夜晚", + "cs_editor.tod_sunrise": "日出", + "cs_editor.tod_sunset": "日落", + "cs_editor.total_objects": "总对象数:", + "cs_editor.trans_delta": "过渡增量", + "cs_editor.transitioning": "正在转换", + "cs_editor.type": "类型", + "cs_editor.ui_scale": "UI缩放", + "cs_editor.undo_no_changes": "撤消(Ctrl+Z) - 没有可撤消的更改", + "cs_editor.undone_changes_to": "撤销了对 {} 的更改", + "cs_editor.unknown": "未知", + "cs_editor.unlock": "解锁", + "cs_editor.unlock_weather": "解锁天气", + "cs_editor.unnamed_cell": "[未命名单元]", + "cs_editor.unpause_all": "全部取消暂停", + "cs_editor.unsaved_changes": "(未保存的更改)", + "cs_editor.unsaved_changes_tooltip": "未保存的更改 - 点击保存进行保留", + "cs_editor.unsupported_type": "(不支持的类型)", + "cs_editor.unsupported_variable_type": "不支持的变量类型", + "cs_editor.unsupported_variable_type_tooltip": "此变量类型尚未有自定义UI实现。上方的原始JSON值将被显示。", + "cs_editor.use_inherit_checkboxes": "使用\"从父级继承\"复选框复制特定值。", + "cs_editor.use_text_buttons": "使用文本按钮代替图标", + "cs_editor.used_times": "使用了 {} 次", + "cs_editor.user_settings": "用户设置", + "cs_editor.using_global_settings": "使用全局设置", + "cs_editor.using_weather_specific_settings": "使用天气特定设置", + "cs_editor.value": "值", + "cs_editor.values": "值", + "cs_editor.values_3_plus": "(使用3次以上的值将显示在此处)", + "cs_editor.vanilla_speed": "原版速度", + "cs_editor.velocity": "速度", + "cs_editor.viewport": "视口", + "cs_editor.viewport_unavailable": "视口不可用", + "cs_editor.viewport_unavailable_hdr": "启用HDR显示时视口不可用", + "cs_editor.visual_effect_begin": "视觉效果开始", + "cs_editor.visual_effect_end": "视觉效果结束", + "cs_editor.volume": "音量", + "cs_editor.volumetric_lighting_label": "体积光照:", + "cs_editor.weather_lighting_browser": "CS 编辑器浏览器", + "cs_editor.weathers_count": "天气:%d", + "cs_editor.widget_type_cell_lighting": "单元光照", + "cs_editor.widget_type_imagespace": "图像空间", + "cs_editor.widget_type_lens_flare": "镜头光晕", + "cs_editor.widget_type_lighting": "光照", + "cs_editor.widget_type_precipitation": "降水", + "cs_editor.widget_type_visual_effect": "视觉效果", + "cs_editor.widget_type_volumetric_lighting": "体积光照", + "cs_editor.widget_type_weather": "天气", + "cs_editor.wind_direction_label": "风向", + "cs_editor.wind_direction_range_label": "风向范围", + "cs_editor.wind_speed": "风速", + "cs_editor.window": "窗口", + "cs_editor.xy_rotation": "XY 旋转", + "cs_editor.yes_delete": "是,删除", + "cs_editor.z_rotation": "Z 旋转", + "feature.cs_editor.accelerate_weather_change": "加速天气变化", + "feature.cs_editor.accelerate_weather_change_tooltip": "启用时,天气变化即时生效", + "feature.cs_editor.aurora": "极光", + "feature.cs_editor.aurora_sun": "极光太阳", + "feature.cs_editor.clear_all": "清除全部", + "feature.cs_editor.cloudy": "多云", + "feature.cs_editor.collapse": "折叠", + "feature.cs_editor.current_weather": "当前天气:%s", + "feature.cs_editor.current_weather_column": "当前天气", + "feature.cs_editor.effective_wind_dir": "有效风向:%.1f°(原始 - %.1f°)", + "feature.cs_editor.expand": "展开", + "feature.cs_editor.feature_weather_analysis_tooltip_0": "天气分析提供方:", + "feature.cs_editor.feature_weather_analysis_tooltip_1": "功能类别:", + "feature.cs_editor.feature_weather_analysis_tooltip_2": "点击以%s此功能的天气数据", + "feature.cs_editor.filter_by_weather_type": "按天气类型筛选:", + "feature.cs_editor.has_custom_settings": "有自定义设置", + "feature.cs_editor.headwind": "逆风(风朝向玩家)", + "feature.cs_editor.last_weather_column": "上次天气", + "feature.cs_editor.left_crosswind": "左侧横风", + "feature.cs_editor.lightning_begin_fade_in": "闪电开始淡入:%.3f(原始%u)", + "feature.cs_editor.lightning_color": "闪电颜色:", + "feature.cs_editor.lightning_end_fade_out": "闪电结束淡出:%.3f(原始%u)", + "feature.cs_editor.lightning_fade_info_0": "闪电淡入淡出过渡参数:", + "feature.cs_editor.lightning_fade_info_1": "开始淡入:闪电开始出现的点", + "feature.cs_editor.lightning_fade_info_2": "结束淡出:闪电完全消失的点", + "feature.cs_editor.lightning_fade_info_3": "原始值:0-255(uint8),归一化:0.0-1.0", + "feature.cs_editor.lock_weather": "锁定天气", + "feature.cs_editor.no_active_weather": "无活跃天气", + "feature.cs_editor.no_precipitation_data": "粒子密度:无降水数据", + "feature.cs_editor.no_transition": "过渡来源:无过渡", + "feature.cs_editor.no_weather_found": "未找到天气", + "feature.cs_editor.none_filter": "无", + "feature.cs_editor.none_filter_tooltip_0": "显示未分类到任何特定类别的天气。", + "feature.cs_editor.none_filter_tooltip_1": "包括无标记或仅有未追踪标记的天气。", + "feature.cs_editor.none_filter_tooltip_2": "追踪的类别:晴朗、多云、雨天、雪天、极光、极光日照", + "feature.cs_editor.open_editor": "打开 CS 编辑器", + "feature.cs_editor.particle_density": "粒子密度:%.3f", + "feature.cs_editor.particle_texture": "粒子纹理:%s", + "feature.cs_editor.particle_texture_none": "粒子纹理:无", + "feature.cs_editor.player_direction": "玩家方向:%.1f°", + "feature.cs_editor.pleasant": "晴朗", + "feature.cs_editor.precip_begin_fade_in": "降水开始淡入:%.3f(原始%u)", + "feature.cs_editor.precip_end_fade_out": "降水结束淡出:%.3f(原始%u)", + "feature.cs_editor.precip_fade_info_0": "降水淡入淡出过渡参数:", + "feature.cs_editor.precip_fade_info_1": "开始淡入:降水开始出现的点", + "feature.cs_editor.precip_fade_info_2": "结束淡出:降水完全消失的点", + "feature.cs_editor.precip_fade_info_3": "原始值:0-255(uint8),归一化:0.0-1.0", + "feature.cs_editor.rainy": "下雨", + "feature.cs_editor.reset_weather": "重置天气", + "feature.cs_editor.reset_weather_tooltip": "将天气重置为默认值", + "feature.cs_editor.right_crosswind": "右侧横风", + "feature.cs_editor.select_all": "全选", + "feature.cs_editor.select_weather": "选择天气", + "feature.cs_editor.show_in_overlay": "在叠加层中显示", + "feature.cs_editor.show_in_overlay_tooltip": "在单独的窗口中打开天气详情,即使主菜单关闭也保持打开。", + "feature.cs_editor.sky_not_available": "天空不可用", + "feature.cs_editor.sky_not_full": "天空未处于完整模式", + "feature.cs_editor.sky_wind_speed": "天空风速:%.2f", + "feature.cs_editor.sky_wind_tooltip_0": "天空系统的当前活跃风速", + "feature.cs_editor.sky_wind_tooltip_1": "这会影响粒子行为和基于风的效果", + "feature.cs_editor.snow": "下雪", + "feature.cs_editor.tailwind": "顺风(风在玩家背后)", + "feature.cs_editor.thunder_freq_info_0": "雷声频率原始值(0-255):", + "feature.cs_editor.thunder_freq_info_1": "Creation Kit滑块的已知数据点:", + "feature.cs_editor.thunder_freq_info_2": "- 原始值15 = ~100%频率(最高雷声)", + "feature.cs_editor.thunder_freq_info_3": "- 原始值76 = ~75%频率", + "feature.cs_editor.thunder_freq_info_4": "- 原始值203 = ~20%频率", + "feature.cs_editor.thunder_freq_info_5": "- 原始值246 = ~5%频率", + "feature.cs_editor.thunder_freq_info_6": "- 原始值255 = ~0%频率(最低雷声)", + "feature.cs_editor.thunder_freq_info_7": "范围:0-255(无符号8位整数)", + "feature.cs_editor.thunder_freq_info_8": "注意:Creation Kit以非线性方式解释此值", + "feature.cs_editor.thunder_frequency": "雷声频率:%u", + "feature.cs_editor.time_controls": "时间控制", + "feature.cs_editor.toggle_with": "切换键:", + "feature.cs_editor.tooltip_editor_id": "编辑器ID:%s", + "feature.cs_editor.tooltip_editor_id_2": "编辑器ID:%s", + "feature.cs_editor.tooltip_flags": "标志:%s", + "feature.cs_editor.tooltip_flags_none": "标志:无", + "feature.cs_editor.tooltip_form_id": "表单ID:0x%08X", + "feature.cs_editor.tooltip_form_id_2": "表单ID:0x%08X", + "feature.cs_editor.tooltip_name": "名称:%s", + "feature.cs_editor.tooltip_weather_name": "天气:%s", + "feature.cs_editor.transition_progress": "过渡:{:.1f}%", + "feature.cs_editor.transitioning_from": "过渡来源:%s", + "feature.cs_editor.unknown": "未知", + "feature.cs_editor.unlock_weather": "解锁天气", + "feature.cs_editor.using_default_settings": "使用默认设置", + "feature.cs_editor.weather": "天气", + "feature.cs_editor.weather_controls": "天气控制", + "feature.cs_editor.weather_details": "天气详情", + "feature.cs_editor.weather_information": "天气信息", + "feature.cs_editor.weather_percentage": "天气百分比:%.1f%%", + "feature.cs_editor.weather_status": "天气状态", + "feature.cs_editor.weather_wind_speed": "天气风速:%.2f(原始%d)", + "feature.cs_editor.wind_direction": "风向:%.1f°(原始%d)", + "feature.cs_editor.wind_direction_range": "风向范围:%.1f°(原始%d)", + "feature.cs_editor.wind_direction_tooltip_0": "天气定义中的风向", + "feature.cs_editor.wind_speed_tooltip_0": "天气定义中的风速", + "feature.cs_editor.wind_vs_player": "风与玩家夹角:%.1f°", + "feature.cs_editor.wind_vs_player_tooltip_0": "风相对于玩家方向:", + "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = 顺风(风在玩家背后)", + "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = 横风(左/右)", + "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = 逆风(风朝向玩家)", + "cs_editor.close_cs_editor": "关闭 CS 编辑器(Esc)", + "cs_editor.cs_editor": "CS 编辑器", + "cs_editor.parent_cs_editor_feature": "仅编辑器功能:设置父级天气以从中复制设置。" +} diff --git a/src/CSEditor/EditorWindow.cpp b/src/CSEditor/EditorWindow.cpp index 30077298f4..b5c5b050d1 100644 --- a/src/CSEditor/EditorWindow.cpp +++ b/src/CSEditor/EditorWindow.cpp @@ -1,8 +1,9 @@ #include "EditorWindow.h" +#include "../I18n/I18n.h" +#include "Features/CSEditor.h" #include "Features/HDRDisplay.h" #include "Features/Upscaling.h" -#include "Features/CSEditor.h" #include "Globals.h" #include "InteriorOnlyPanel.h" #include "Menu.h" @@ -14,6 +15,8 @@ #include "WeatherUtils.h" #include "imgui_internal.h" +#define I18N_KEY_PREFIX "cs_editor." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings::PaletteColorEntry, r, g, b, useCount, lastUsedTime, isFavorite) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings::PaletteFavoriteColor, hasValue, r, g, b) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, showViewport, widgetTypeSizes, paletteColors, paletteFavorites) @@ -117,7 +120,25 @@ bool IconButton(const char* label, bool filled, const char* iconType) namespace { - constexpr const char* kFilterColumnNames[] = { "All", "Editor ID", "Form ID", "File", "Status" }; + const char* GetFilterColumnName(int index) + { + switch (index) { + case 0: + return T(TKEY("filter_all"), "All"); + case 1: + return T(TKEY("filter_editor_id"), "Editor ID"); + case 2: + return T(TKEY("filter_form_id"), "Form ID"); + case 3: + return T(TKEY("filter_file"), "File"); + case 4: + return T(TKEY("filter_status"), "Status"); + default: + return ""; + } + } + + constexpr int kFilterColumnCount = 5; } // namespace void EditorWindow::ResetObjectsFilter() @@ -130,8 +151,8 @@ void EditorWindow::ResetObjectsFilter() bool EditorWindow::MatchesObjectFilter(Widget* w) const { - static_assert(static_cast(FilterColumn::Count_) == IM_ARRAYSIZE(kFilterColumnNames), - "kFilterColumnNames must have one entry per FilterColumn value"); + static_assert(static_cast(FilterColumn::Count_) == kFilterColumnCount, + "kFilterColumnCount must match FilterColumn enum"); if (!w) return false; if (m_filterBuffer[0] == '\0') @@ -180,7 +201,7 @@ std::string EditorWindow::ResolveEditorId(RE::TESForm* form, const WidgetVec& wi void EditorWindow::ShowObjectsWindow() { - Util::BeginWithRoundedClose("CS Editor Browser", nullptr); + Util::BeginWithRoundedClose(T(TKEY("weather_lighting_browser"), "CS Editor Browser"), nullptr); // Reset filter state when the user switches categories so stale column // selections (e.g. Status) don't hide all items in the new category. @@ -193,8 +214,8 @@ void EditorWindow::ShowObjectsWindow() if (ImGui::BeginTable("ObjectTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner)) { // Fixed categories column, objects column fills remaining width const float categoriesWidth = 180.0f * Util::GetUIScale(); - ImGui::TableSetupColumn("Categories", ImGuiTableColumnFlags_WidthFixed, categoriesWidth); - ImGui::TableSetupColumn("Objects", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T(TKEY("categories"), "Categories"), ImGuiTableColumnFlags_WidthFixed, categoriesWidth); + ImGui::TableSetupColumn(T(TKEY("objects"), "Objects"), ImGuiTableColumnFlags_WidthStretch); ImGui::TableNextRow(); @@ -207,19 +228,32 @@ void EditorWindow::ShowObjectsWindow() ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4()); if (ImGui::BeginListBox("##CategoriesList", { -FLT_MIN, -FLT_MIN })) { - ImGui::Text("Categories"); + ImGui::Text("%s", T(TKEY("categories"), "Categories")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - // List of categories - const char* categories[] = { "Weather", "ImageSpace", "Lighting Template", "Cell Lighting", - "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect", - "Interior Only", "Light Editor" }; + struct CategoryOption + { + const char* id; + const char* label; + }; + const CategoryOption categories[] = { + { "Weather", T(TKEY("category_weather"), "Weather") }, + { "ImageSpace", T(TKEY("category_imagespace"), "ImageSpace") }, + { "Lighting Template", T(TKEY("category_lighting_template"), "Lighting Template") }, + { "Cell Lighting", T(TKEY("category_cell_lighting"), "Cell Lighting") }, + { "Volumetric Lighting", T(TKEY("category_volumetric_lighting"), "Volumetric Lighting") }, + { "Shader Particle Geometry", T(TKEY("category_shader_particle"), "Shader Particle Geometry") }, + { "Lens Flare", T(TKEY("category_lens_flare"), "Lens Flare") }, + { "Visual Effect", T(TKEY("category_visual_effect"), "Visual Effect") }, + { "Interior Only", T(TKEY("category_interior_only"), "Interior Only") }, + { "Light Editor", T(TKEY("category_lighting_editor"), "Light Editor") } + }; for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { // Highlight the selected category - if (ImGui::Selectable(categories[i], m_selectedCategory == categories[i])) { - m_selectedCategory = categories[i]; // Update selected category + if (ImGui::Selectable(categories[i].label, m_selectedCategory == categories[i].id)) { + m_selectedCategory = categories[i].id; // Keep the stable English ID internally } } ImGui::EndListBox(); @@ -272,7 +306,8 @@ void EditorWindow::ShowObjectsWindow() }; // Build active records for the current category tab - struct ActiveRecord { + struct ActiveRecord + { std::string label; std::string suffix; RE::FormID formId; @@ -297,15 +332,17 @@ void EditorWindow::ShowObjectsWindow() }; auto addSingle = [&](RE::TESForm* form, const WidgetVec& widgets, std::string suffix = "") { - if (!form) return; + if (!form) + return; auto id = form->GetFormID(); activeRecords.push_back({ ResolveEditorId(form, widgets), std::move(suffix), id, openByFormId(id, &widgets) }); }; - auto addTOD = [&](auto* (&fields)[RE::TESWeather::ColorTimes::kTotal], const WidgetVec& widgets) { + auto addTOD = [&](auto*(&fields)[RE::TESWeather::ColorTimes::kTotal], const WidgetVec& widgets) { for (int tod = 0; tod < RE::TESWeather::ColorTimes::kTotal; ++tod) { auto* form = fields[tod]; - if (!form) continue; + if (!form) + continue; auto id = form->GetFormID(); bool already = std::any_of(activeRecords.begin(), activeRecords.end(), [&](const ActiveRecord& r) { return r.formId == id; }); @@ -315,7 +352,8 @@ void EditorWindow::ShowObjectsWindow() }; auto addWeather = [&](RE::TESWeather* weatherRecord, std::string suffix = "") { - if (!weatherRecord) return; + if (!weatherRecord) + return; auto id = weatherRecord->GetFormID(); activeRecords.push_back({ ResolveEditorId(weatherRecord, weatherWidgets), std::move(suffix), id, openByFormId(id, &weatherWidgets) }); }; @@ -323,9 +361,10 @@ void EditorWindow::ShowObjectsWindow() if (m_selectedCategory == "Weather") { addWeather(weather); if (sky && sky->lastWeather != weather) - addWeather(sky->lastWeather, "transitioning"); + addWeather(sky->lastWeather, T(TKEY("transitioning"), "transitioning")); } else if (m_selectedCategory == "ImageSpace") { - if (weather) addTOD(weather->imageSpaces, imageSpaceWidgets); + if (weather) + addTOD(weather->imageSpaces, imageSpaceWidgets); } else if (m_selectedCategory == "Lighting Template") { auto* player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell) @@ -335,7 +374,7 @@ void EditorWindow::ShowObjectsWindow() if (player && player->parentCell && player->parentCell->IsInteriorCell()) { auto* cell = player->parentCell; const char* cellName = cell->GetName(); - std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; + std::string displayName = cellName && cellName[0] ? cellName : T(TKEY("unnamed_cell"), "[Unnamed Cell]"); activeRecords.push_back({ std::move(displayName), "", cell->GetFormID(), [this, cell]() { if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { @@ -351,13 +390,17 @@ void EditorWindow::ShowObjectsWindow() } }); } } else if (m_selectedCategory == "Volumetric Lighting") { - if (weather) addTOD(weather->volumetricLighting, volumetricLightingWidgets); + if (weather) + addTOD(weather->volumetricLighting, volumetricLightingWidgets); } else if (m_selectedCategory == "Shader Particle Geometry") { - if (weather) addSingle(weather->precipitationData, precipitationWidgets); + if (weather) + addSingle(weather->precipitationData, precipitationWidgets); } else if (m_selectedCategory == "Lens Flare") { - if (weather) addSingle(weather->sunGlareLensFlare, lensFlareWidgets); + if (weather) + addSingle(weather->sunGlareLensFlare, lensFlareWidgets); } else if (m_selectedCategory == "Visual Effect") { - if (weather) addSingle(weather->referenceEffect, referenceEffectWidgets); + if (weather) + addSingle(weather->referenceEffect, referenceEffectWidgets); } // Fall back to current weather when the active category has no active record @@ -372,7 +415,7 @@ void EditorWindow::ShowObjectsWindow() const auto& theme = Menu::GetSingleton()->GetTheme(); ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.RestartNeeded); - ImGui::Text("Active:"); + ImGui::Text("%s", T(TKEY("active"), "Active:")); ImGui::PopStyleColor(); ImGui::SameLine(); const float recordX = ImGui::GetCursorPosX(); @@ -392,7 +435,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::TextDisabled("(0x%08X)", rec.formId); ImGui::SameLine(); char btnId[32]; - snprintf(btnId, sizeof(btnId), "Open##active_%d", i); + snprintf(btnId, sizeof(btnId), "%s##active_%d", T(TKEY("open"), "Open"), i); if (ImGui::SmallButton(btnId)) rec.open(); } @@ -418,26 +461,35 @@ void EditorWindow::ShowObjectsWindow() // Fixed width is the sum of every item that follows the search bar on the same row. // Each SameLine() contributes style.ItemSpacing.x; widths are listed explicitly // so adding or removing a widget only requires updating its own expression. + const char* favoritesText = T(TKEY("favorites"), "Favorites"); + const char* flaggedText = T(TKEY("flagged"), "Flagged"); const float fixedW = - style.ItemSpacing.x + comboW + // combo - style.ItemSpacing.x + helpW + // help marker - style.ItemSpacing.x + spacerW + // spacer before favorites - style.ItemSpacing.x + iconW + // fav icon - style.ItemSpacing.x + ImGui::CalcTextSize("Favorites").x + // "Favorites" label - style.ItemSpacing.x + spacerW + // spacer before flagged - style.ItemSpacing.x + iconW + // flag icon - style.ItemSpacing.x + ImGui::CalcTextSize("Flagged").x; // "Flagged" label + style.ItemSpacing.x + comboW + // combo + style.ItemSpacing.x + helpW + // help marker + style.ItemSpacing.x + spacerW + // spacer before favorites + style.ItemSpacing.x + iconW + // fav icon + style.ItemSpacing.x + ImGui::CalcTextSize(favoritesText).x + // "Favorites" label + style.ItemSpacing.x + spacerW + // spacer before flagged + style.ItemSpacing.x + iconW + // flag icon + style.ItemSpacing.x + ImGui::CalcTextSize(flaggedText).x; // "Flagged" label ImGui::SetNextItemWidth(std::max(50.0f, ImGui::GetContentRegionAvail().x - fixedW)); - ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", m_filterBuffer, sizeof(m_filterBuffer)); + ImGui::InputTextWithHint("##ObjectFilter", T(TKEY("filter_hint"), "Filter... (Ctrl+F)"), m_filterBuffer, sizeof(m_filterBuffer)); ImGui::SameLine(); ImGui::SetNextItemWidth(comboW); int col = static_cast(m_currentFilterColumn); - if (ImGui::Combo("##FilterBy", &col, kFilterColumnNames, IM_ARRAYSIZE(kFilterColumnNames))) + if (ImGui::Combo("##FilterBy", &col, [](void*, int idx, const char** out) -> bool { + *out = GetFilterColumnName(idx); + return true; }, nullptr, kFilterColumnCount)) m_currentFilterColumn = static_cast(col); ImGui::SameLine(); - Util::HelpMarker("Filter the object list by the selected column.\nAll: searches Editor ID, Form ID, File, and Status.\nStatus: hides items with no status marker when the search box is non-empty.\nCtrl+F: Focus search\nEnter: Open selected"); + Util::HelpMarker(T(TKEY("filter_help"), + "Filter the object list by the selected column.\n" + "All: searches Editor ID, Form ID, File, and Status.\n" + "Status: hides items with no status marker when the search box is non-empty.\n" + "Ctrl+F: Focus search\n" + "Enter: Open selected")); // Quick filter buttons const ImVec2 filterSpacer(spacerW, 0.0f); @@ -448,7 +500,7 @@ void EditorWindow::ShowObjectsWindow() m_showOnlyFavorites = !m_showOnlyFavorites; } ImGui::SameLine(); - ImGui::Text("Favorites"); + ImGui::Text("%s", favoritesText); ImGui::SameLine(); ImGui::Dummy(filterSpacer); @@ -457,13 +509,13 @@ void EditorWindow::ShowObjectsWindow() m_showOnlyFlagged = !m_showOnlyFlagged; } ImGui::SameLine(); - ImGui::Text("Flagged"); + ImGui::Text("%s", flaggedText); // Show recent widgets section for current category auto recentIt = settings.recentWidgets.find(m_selectedCategory); if (recentIt != settings.recentWidgets.end() && !recentIt->second.empty()) { ImGui::Spacing(); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Recent:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "%s", T(TKEY("recent"), "Recent:")); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { if (i > 0) @@ -497,12 +549,12 @@ void EditorWindow::ShowObjectsWindow() // Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation. if (ImGui::BeginTable("DetailsTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Sortable)) { - ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 38.0f * scale, ColFav); // Favorite indicator - ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f, ColEditorID); // Largest - weather/template names - ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 90.0f * scale, ColFormID); // Fixed - 8 hex chars - ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f, ColFile); // Medium - plugin names - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f, ColStatus); // Smaller - status text - ImGui::TableSetupColumn("json", ImGuiTableColumnFlags_WidthFixed, 55.0f * scale, ColJson); // JSON file / delete + ImGui::TableSetupColumn(T(TKEY("fav"), "Fav"), ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 38.0f * scale, ColFav); // Favorite indicator + ImGui::TableSetupColumn(T(TKEY("editor_id"), "Editor ID"), ImGuiTableColumnFlags_WidthStretch, 3.5f, ColEditorID); // Largest - weather/template names + ImGui::TableSetupColumn(T(TKEY("form_id"), "Form ID"), ImGuiTableColumnFlags_WidthFixed, 90.0f * scale, ColFormID); // Fixed - 8 hex chars + ImGui::TableSetupColumn(T(TKEY("file"), "File"), ImGuiTableColumnFlags_WidthStretch, 2.0f, ColFile); // Medium - plugin names + ImGui::TableSetupColumn(T(TKEY("status"), "Status"), ImGuiTableColumnFlags_WidthStretch, 1.5f, ColStatus); // Smaller - status text + ImGui::TableSetupColumn(T(TKEY("json"), "json"), ImGuiTableColumnFlags_WidthFixed, 55.0f * scale, ColJson); // JSON file / delete ImGui::TableHeadersRow(); @@ -599,7 +651,7 @@ void EditorWindow::ShowObjectsWindow() pendingDeleteWidget = widget; pendingDeletePopupRequested = true; } - Util::AddTooltip("Delete JSON file"); + Util::AddTooltip(T(TKEY("delete_json_file"), "Delete JSON file")); } } }; @@ -665,7 +717,7 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); - ImGui::Text("Interior Cell"); + ImGui::Text("%s", T(TKEY("interior_cell"), "Interior Cell")); // json column (empty for cells - no standalone json) ImGui::TableNextColumn(); @@ -674,8 +726,8 @@ void EditorWindow::ShowObjectsWindow() ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Warning, "Cell Lighting is only available for interior cells."); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Disable, "You are currently in an exterior cell."); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Warning, "%s", T(TKEY("cell_lighting_interior_only"), "Cell Lighting is only available for interior cells.")); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Disable, "%s", T(TKEY("currently_exterior_cell"), "You are currently in an exterior cell.")); ImGui::PopTextWrapPos(); } } else { @@ -683,7 +735,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Error, "Player cell not available."); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Error, "%s", T(TKEY("player_cell_unavailable"), "Player cell not available.")); ImGui::PopTextWrapPos(); } } @@ -748,19 +800,19 @@ void EditorWindow::ShowObjectsWindow() Util::SetTooltipPositionNearMouse(estimatedTooltipHeight); if (ImGui::BeginTooltip()) { // ImageSpace info - use widget cache for proper editor IDs - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "ImageSpace:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "%s", T(TKEY("imagespace_label"), "ImageSpace:")); for (int tod = 0; tod < 4; tod++) { auto name = ResolveEditorId(weatherWidget->weather->imageSpaces[tod], imageSpaceWidgets); - ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), name.empty() ? "None" : name.c_str()); + ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), name.empty() ? T(TKEY("none_filter"), "None") : name.c_str()); } ImGui::Spacing(); // VolumetricLighting info - show short local FormID only - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Volumetric Lighting:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "%s", T(TKEY("volumetric_lighting_label"), "Volumetric Lighting:")); for (int tod = 0; tod < 4; tod++) { auto* f = weatherWidget->weather->volumetricLighting[tod]; - ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), f ? std::format("0x{:X}", f->GetLocalFormID()).c_str() : "None"); + ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), f ? std::format("0x{:X}", f->GetLocalFormID()).c_str() : T(TKEY("none_filter"), "None")); } ImGui::EndTooltip(); } @@ -785,7 +837,7 @@ void EditorWindow::ShowObjectsWindow() } } - if (ImGui::MenuItem("Remove")) { + if (ImGui::MenuItem(T(TKEY("remove"), "Remove"))) { markedRecords.erase(editorLabel); Save(); } @@ -845,7 +897,7 @@ void EditorWindow::ShowObjectsWindow() void EditorWindow::ShowViewportWindow() { - Util::BeginWithRoundedClose("Viewport", nullptr, ImGuiWindowFlags_NoFocusOnAppearing); + Util::BeginWithRoundedClose(T(TKEY("viewport"), "Viewport"), nullptr, ImGuiWindowFlags_NoFocusOnAppearing); // The size of the image in ImGui // Get the available space in the current window ImVec2 availableSpace = ImGui::GetContentRegionAvail(); @@ -868,7 +920,7 @@ void EditorWindow::ShowViewportWindow() if (tempTexture && tempTexture->srv) { ImGui::Image((void*)tempTexture->srv.get(), imageSize); } else { - ImGui::TextDisabled("Viewport unavailable"); + ImGui::TextDisabled("%s", T(TKEY("viewport_unavailable"), "Viewport unavailable")); } ImGui::End(); @@ -923,13 +975,13 @@ void EditorWindow::RenderUI() ImVec2(window->ClipRect.Max.x, window->ClipRect.Max.y - borderInset), true); } - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { + if (ImGui::BeginMenu(T(TKEY("file"), "File"))) { + if (ImGui::MenuItem(T(TKEY("save_all_open_widgets"), "Save All Open Widgets"), "Ctrl+S")) { SaveAll(); } // Save individual widgets submenu - if (ImGui::BeginMenu("Save")) { + if (ImGui::BeginMenu(T(TKEY("save"), "Save"))) { bool hasOpen = false; for (auto* collection : GetWidgetCollections()) hasOpen = WidgetFactory::DrawSaveWidgetMenuItems(*collection, hasOpen); @@ -941,7 +993,7 @@ void EditorWindow::RenderUI() } if (!hasOpen) - ImGui::TextDisabled("No open widgets"); + ImGui::TextDisabled("%s", T(TKEY("no_open_widgets"), "No open widgets")); ImGui::EndMenu(); } @@ -951,12 +1003,12 @@ void EditorWindow::RenderUI() WidgetFactory::DrawCloseAllMenuItem(*collection); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Settings")) { - if (ImGui::MenuItem("General Settings")) { + if (ImGui::BeginMenu(T(TKEY("settings"), "Settings"))) { + if (ImGui::MenuItem(T(TKEY("general_settings"), "General Settings"))) { showSettingsWindow = true; settingsSelectedCategory = "General"; } - if (ImGui::MenuItem("Editor Flags")) { + if (ImGui::MenuItem(T(TKEY("editor_flags"), "Editor Flags"))) { showSettingsWindow = true; settingsSelectedCategory = "Flags"; } @@ -965,7 +1017,7 @@ void EditorWindow::RenderUI() // Current cell lighting auto player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell && player->parentCell->IsInteriorCell()) { - if (ImGui::MenuItem("Edit Current Cell Lighting")) { + if (ImGui::MenuItem(T(TKEY("edit_current_cell_lighting"), "Edit Current Cell Lighting"))) { // Check if widget already exists bool found = false; if (currentCellLightingWidget && currentCellLightingWidget->cell == player->parentCell) { @@ -983,45 +1035,45 @@ void EditorWindow::RenderUI() } } else { ImGui::BeginDisabled(); - ImGui::MenuItem("Edit Current Cell Lighting"); + ImGui::MenuItem(T(TKEY("edit_current_cell_lighting"), "Edit Current Cell Lighting")); ImGui::EndDisabled(); - Util::AddTooltip("Only available in interior cells", ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled); + Util::AddTooltip(T(TKEY("interior_only_available"), "Only available in interior cells"), ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled); } ImGui::Separator(); - if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { + if (ImGui::Checkbox(T(TKEY("auto_apply_changes"), "Auto-Apply Changes"), &settings.autoApplyChanges)) { Save(); } - Util::AddTooltip("Automatically apply weather changes to the game as you edit"); + Util::AddTooltip(T(TKEY("auto_apply_changes_tooltip"), "Automatically apply weather changes to the game as you edit")); - if (ImGui::Checkbox("Enable Inherit From Parent", &settings.enableInheritFromParent)) { + if (ImGui::Checkbox(T(TKEY("enable_inherit_from_parent"), "Enable Inherit From Parent"), &settings.enableInheritFromParent)) { Save(); } - Util::AddTooltip("Show inherit from parent options in weather widgets"); + Util::AddTooltip(T(TKEY("enable_inherit_tooltip"), "Show inherit from parent options in weather widgets")); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Window")) { + if (ImGui::BeginMenu(T(TKEY("window"), "Window"))) { const bool hdrActive = globals::features::hdrDisplay.loaded && globals::features::hdrDisplay.settings.enableHDR; if (hdrActive) ImGui::BeginDisabled(); - if (ImGui::Checkbox("Viewport", &settings.showViewport)) { + if (ImGui::Checkbox(T(TKEY("viewport"), "Viewport"), &settings.showViewport)) { BackgroundBlur::SetCSEditorActive(settings.showViewport); Save(); } if (hdrActive) { ImGui::EndDisabled(); - Util::AddTooltip("Viewport is unavailable when HDR Display is enabled", ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled); + Util::AddTooltip(T(TKEY("viewport_unavailable_hdr"), "Viewport is unavailable when HDR Display is enabled"), ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled); } - if (ImGui::Checkbox("Palette", &PaletteWindow::GetSingleton()->open)) { + if (ImGui::Checkbox(T(TKEY("palette"), "Palette"), &PaletteWindow::GetSingleton()->open)) { } - if (ImGui::MenuItem("Reset Window Layout")) { + if (ImGui::MenuItem(T(TKEY("reset_window_layout"), "Reset Window Layout"))) { resetLayout = true; } ImGui::Separator(); - ImGui::Text("Open Widgets:"); + ImGui::Text("%s", T(TKEY("open_widgets"), "Open Widgets:")); int openCount = 0; for (auto* collection : GetWidgetCollections()) @@ -1029,47 +1081,47 @@ void EditorWindow::RenderUI() if (currentCellLightingWidget && currentCellLightingWidget->IsOpen()) { ++openCount; - if (ImGui::MenuItem(std::format("{}: {}", currentCellLightingWidget->GetWidgetTypeName(), currentCellLightingWidget->GetEditorID()).c_str())) + if (ImGui::MenuItem(std::format("{}: {}", WidgetFactory::TranslateWidgetTypeName(currentCellLightingWidget->GetWidgetTypeName()), currentCellLightingWidget->GetEditorID()).c_str())) ImGui::SetWindowFocus(currentCellLightingWidget->GetWindowTitle().c_str()); } if (openCount == 0) - ImGui::TextDisabled("No widgets open"); + ImGui::TextDisabled("%s", T(TKEY("no_widgets_open"), "No widgets open")); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Help")) { - ImGui::Text("CS Editor"); + if (ImGui::BeginMenu(T(TKEY("help"), "Help"))) { + ImGui::Text("%s", T(TKEY("cs_editor"), "CS Editor")); ImGui::Separator(); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Keyboard Shortcuts:"); - ImGui::BulletText("Ctrl+F: Focus search"); - ImGui::BulletText("Ctrl+S: Save all open widgets"); - ImGui::BulletText("Ctrl+W: Close focused widget"); - ImGui::BulletText("Enter: Open selected widget"); - ImGui::BulletText("Esc: Close editor"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "%s", T(TKEY("keyboard_shortcuts"), "Keyboard Shortcuts:")); + ImGui::BulletText("%s", T(TKEY("shortcut_ctrl_f"), "Ctrl+F: Focus search")); + ImGui::BulletText("%s", T(TKEY("shortcut_ctrl_s"), "Ctrl+S: Save all open widgets")); + ImGui::BulletText("%s", T(TKEY("shortcut_ctrl_w"), "Ctrl+W: Close focused widget")); + ImGui::BulletText("%s", T(TKEY("shortcut_enter"), "Enter: Open selected widget")); + ImGui::BulletText("%s", T(TKEY("shortcut_esc"), "Esc: Close editor")); ImGui::Separator(); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Quick Tips:"); - ImGui::BulletText("Double-click to edit"); - ImGui::BulletText("Right-click to mark status"); - ImGui::BulletText("Click star icon to favorite"); - ImGui::BulletText("Use quick filters for fast sorting"); - ImGui::BulletText("Auto-Apply updates game live"); - ImGui::BulletText("Lock weather to prevent changes"); - ImGui::BulletText("Undo button reverts recent changes (Ctrl+Z)"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "%s", T(TKEY("quick_tips"), "Quick Tips:")); + ImGui::BulletText("%s", T(TKEY("tip_double_click"), "Double-click to edit")); + ImGui::BulletText("%s", T(TKEY("tip_right_click"), "Right-click to mark status")); + ImGui::BulletText("%s", T(TKEY("tip_star_favorite"), "Click star icon to favorite")); + ImGui::BulletText("%s", T(TKEY("tip_quick_filters"), "Use quick filters for fast sorting")); + ImGui::BulletText("%s", T(TKEY("tip_auto_apply"), "Auto-Apply updates game live")); + ImGui::BulletText("%s", T(TKEY("tip_lock_weather"), "Lock weather to prevent changes")); + ImGui::BulletText("%s", T(TKEY("tip_undo"), "Undo button reverts recent changes (Ctrl+Z)")); ImGui::Separator(); - ImGui::Text("Total Objects:"); - ImGui::BulletText("Weathers: %d", (int)weatherWidgets.size()); - ImGui::BulletText("Lighting: %d", (int)lightingTemplateWidgets.size()); - ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); + ImGui::Text("%s", T(TKEY("total_objects"), "Total Objects:")); + ImGui::BulletText(T(TKEY("weathers_count"), "Weathers: %d"), (int)weatherWidgets.size()); + ImGui::BulletText(T(TKEY("lighting_count"), "Lighting: %d"), (int)lightingTemplateWidgets.size()); + ImGui::BulletText(T(TKEY("imagespaces_count"), "ImageSpaces: %d"), (int)imageSpaceWidgets.size()); ImGui::Separator(); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.CurrentHotkey, "Favorites: %d", (int)settings.favoriteWidgets.size()); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.CurrentHotkey, T(TKEY("favorites_count"), "Favorites: %d"), (int)settings.favoriteWidgets.size()); // Count total recent widgets across all categories int totalRecent = 0; for (const auto& [category, widgets] : settings.recentWidgets) { totalRecent += static_cast(widgets.size()); } - ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor, "Recent: %d", totalRecent); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor, T(TKEY("recent_count"), "Recent: %d"), totalRecent); ImGui::EndMenu(); } @@ -1095,7 +1147,7 @@ void EditorWindow::RenderUI() ImGui::PopStyleColor(); } ImGui::PopStyleVar(2); - Util::AddTooltip(canUndo ? std::format("Undo (Ctrl+Z) - {} states", (int)undoStack.size()).c_str() : "Undo (Ctrl+Z) - No changes to undo"); + Util::AddTooltip(canUndo ? std::format("Undo (Ctrl+Z) - {} states", (int)undoStack.size()).c_str() : T(TKEY("undo_no_changes"), "Undo (Ctrl+Z) - No changes to undo")); } // Right-aligned items — use SetCursorScreenPos to bypass menu bar GroupOffset @@ -1154,11 +1206,11 @@ void EditorWindow::RenderUI() if (showPreviewStatus) { std::string hotkey = Util::Input::KeyIdToString(menu->GetSettings().CSEditorToggleKey); if (previewMode == PreviewMode::FreeCamera) - std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), " [ %s ] FREE CAMERA (Speed: %.0f)", hotkey.c_str(), flySpeed); + std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), T(TKEY("preview_free_camera"), " [ %s ] FREE CAMERA (Speed: %.0f)"), hotkey.c_str(), flySpeed); else if (previewMode == PreviewMode::FreeCameraLocked) - std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), " [ %s ] FREE CAMERA LOCKED", hotkey.c_str()); + std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), T(TKEY("preview_free_camera_locked"), " [ %s ] FREE CAMERA LOCKED"), hotkey.c_str()); else - std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), " [ %s ] PLAY MODE", hotkey.c_str()); + std::snprintf(previewStatusBuf, sizeof(previewStatusBuf), T(TKEY("preview_play_mode"), " [ %s ] PLAY MODE"), hotkey.c_str()); rightCursor -= itemSpacing + ImGui::CalcTextSize(previewStatusBuf).x; previewStatusX = rightCursor; } @@ -1166,7 +1218,7 @@ void EditorWindow::RenderUI() // Time paused text float timePausedX = 0; bool showTimePaused = IsTimePaused(); - const char* timePausedText = " [TIME PAUSED]"; + const char* timePausedText = T(TKEY("time_paused_status"), " [TIME PAUSED]"); if (showTimePaused) { rightCursor -= itemSpacing + ImGui::CalcTextSize(timePausedText).x; timePausedX = rightCursor; @@ -1178,7 +1230,7 @@ void EditorWindow::RenderUI() bool showWeatherLock = weatherLockActive && lockedWeather; if (showWeatherLock) { const char* weatherName = lockedWeather->GetFormEditorID(); - std::snprintf(weatherLockBuf, sizeof(weatherLockBuf), " [LOCKED: %s]", weatherName ? weatherName : "Unknown"); + std::snprintf(weatherLockBuf, sizeof(weatherLockBuf), T(TKEY("locked_weather_status"), " [LOCKED: %s]"), weatherName ? weatherName : T(TKEY("unknown"), "Unknown")); rightCursor -= itemSpacing + ImGui::CalcTextSize(weatherLockBuf).x; weatherLockX = rightCursor; } @@ -1234,20 +1286,20 @@ void EditorWindow::RenderUI() bool isActive = previewMode == PreviewMode::FreeCamera || previewMode == PreviewMode::FreeCameraLocked; if (DrawToggleIconButton("##FreeCamera", menu->uiIcons.freeCamera.texture, isActive, freeCameraX)) EnterPreviewMode(PreviewMode::FreeCamera); - Util::AddTooltip(isActive ? "Exit Free Camera" : "Free Camera (scroll to adjust speed)"); + Util::AddTooltip(isActive ? T(TKEY("exit_free_camera"), "Exit Free Camera") : T(TKEY("free_camera_scroll"), "Free Camera (scroll to adjust speed)")); } if (hasPlayMode) { bool isActive = previewMode == PreviewMode::PlayMode; if (DrawToggleIconButton("##PlayMode", menu->uiIcons.playMode.texture, isActive, playModeX)) EnterPreviewMode(PreviewMode::PlayMode); - Util::AddTooltip(isActive ? "Exit Play Mode" : "Play Mode - Walk around normally"); + Util::AddTooltip(isActive ? T(TKEY("exit_play_mode"), "Exit Play Mode") : T(TKEY("play_mode_walk"), "Play Mode - Walk around normally")); } if (hasPauseButton) { bool isPaused = IsTimePaused(); if (DrawToggleIconButton("##GlobalPauseTime", menu->uiIcons.pauseTime.texture, isPaused, pauseButtonX)) TogglePause(); - Util::AddTooltip(isPaused ? "Resume Time" : "Pause Time"); + Util::AddTooltip(isPaused ? T(TKEY("resume_time"), "Resume Time") : T(TKEY("pause_time"), "Pause Time")); } // Period text and time slider @@ -1264,7 +1316,7 @@ void EditorWindow::RenderUI() ImGui::SetCursorScreenPos(ImVec2(xButtonX, cursorY)); if (Util::ErrorButton("X", ImVec2(closeButtonSize, closeButtonSize))) open = false; - Util::AddTooltip("Close CS Editor (Esc)"); + Util::AddTooltip(T(TKEY("close_cs_editor"), "Close CS Editor (Esc)")); ImGui::PopClipRect(); // End bottom-border clip rect @@ -1523,72 +1575,80 @@ void EditorWindow::LoadSettings() void EditorWindow::ShowSettingsWindow() { - Util::BeginWithRoundedClose("Settings", &showSettingsWindow); + Util::BeginWithRoundedClose(T(TKEY("settings"), "Settings"), &showSettingsWindow); if (ImGui::BeginTable("SettingsTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { - ImGui::TableSetupColumn("Options", ImGuiTableColumnFlags_WidthStretch, 0.3f); + ImGui::TableSetupColumn(T(TKEY("options"), "Options"), ImGuiTableColumnFlags_WidthStretch, 0.3f); ImGui::TableSetupColumn("##Settings", ImGuiTableColumnFlags_WidthStretch, 0.7f); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - const char* options[] = { "General", "Flags" }; - for (int i = 0; i < IM_ARRAYSIZE(options); ++i) { - if (ImGui::Selectable(options[i], settingsSelectedCategory == options[i])) { - settingsSelectedCategory = options[i]; + struct CategoryOption + { + const char* id; + const char* label; + }; + const CategoryOption options[] = { + { "General", T(TKEY("general"), "General") }, + { "Flags", T(TKEY("flags"), "Flags") } + }; + for (const auto& option : options) { + if (ImGui::Selectable(option.label, settingsSelectedCategory == option.id)) { + settingsSelectedCategory = option.id; } } ImGui::TableSetColumnIndex(1); if (settingsSelectedCategory == "General") { - ImGui::Checkbox("Auto-apply changes", &settings.autoApplyChanges); - Util::AddTooltip("Automatically apply changes to weather/lighting when editing"); + ImGui::Checkbox(T(TKEY("auto_apply_changes"), "Auto-Apply Changes"), &settings.autoApplyChanges); + Util::AddTooltip(T(TKEY("auto_apply_changes_tooltip"), "Automatically apply weather changes to the game as you edit")); - ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); - Util::AddTooltip("Display action buttons as text labels instead of icons"); + ImGui::Checkbox(T(TKEY("use_text_buttons"), "Use text buttons instead of icons"), &settings.useTextButtons); + Util::AddTooltip(T(TKEY("text_buttons_tooltip"), "Display action buttons as text labels instead of icons")); - ImGui::Checkbox("Enable 'Inherit From Parent' feature", &settings.enableInheritFromParent); - Util::AddTooltip("Show checkboxes to copy settings from parent weather (editor-only feature)"); + ImGui::Checkbox(T(TKEY("enable_inherit_feature"), "Enable 'Inherit From Parent' feature"), &settings.enableInheritFromParent); + Util::AddTooltip(T(TKEY("enable_inherit_feature_tooltip"), "Show checkboxes to copy settings from parent weather (editor-only feature)")); ImGui::Separator(); - ImGui::TextUnformatted("UI Scale"); + ImGui::TextUnformatted(T(TKEY("ui_scale"), "UI Scale")); ImGui::Spacing(); - if (ImGui::SliderFloat("Editor UI Scale", &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { + if (ImGui::SliderFloat(T(TKEY("editor_ui_scale"), "Editor UI Scale"), &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { Save(); } - Util::AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); + Util::AddTooltip(T(TKEY("editor_ui_scale_tooltip"), "Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)")); - if (Util::ButtonWithFlash("Reset to 1.0")) { + if (Util::ButtonWithFlash(T(TKEY("reset_to_default"), "Reset to 1.0"))) { settings.editorUIScale = 1.0f; Save(); } ImGui::SameLine(); - Util::AddTooltip("Reset UI scale to default (100%)"); + Util::AddTooltip(T(TKEY("reset_ui_scale_tooltip"), "Reset UI scale to default (100%)")); ImGui::Separator(); - ImGui::TextUnformatted("Session & History"); + ImGui::TextUnformatted(T(TKEY("session_history"), "Session & History")); ImGui::Spacing(); - ImGui::SliderInt("Max recent widgets", &settings.maxRecentWidgets, 5, 20); - Util::AddTooltip("Maximum number of recent widgets to remember"); + ImGui::SliderInt(T(TKEY("max_recent_widgets"), "Max recent widgets"), &settings.maxRecentWidgets, 5, 20); + Util::AddTooltip(T(TKEY("max_recent_widgets_tooltip"), "Maximum number of recent widgets to remember")); - if (Util::ButtonWithFlash("Clear Recent History")) { + if (Util::ButtonWithFlash(T(TKEY("clear_recent_history"), "Clear Recent History"))) { settings.recentWidgets.clear(); Save(); } ImGui::SameLine(); - if (Util::ButtonWithFlash("Clear Favorites")) { + if (Util::ButtonWithFlash(T(TKEY("clear_favorites"), "Clear Favorites"))) { settings.favoriteWidgets.clear(); Save(); } } else if (settingsSelectedCategory == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Colour", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 60.0f * Util::GetUIScale()); + ImGui::TableSetupColumn(T(TKEY("label"), "Label"), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T(TKEY("colour"), "Colour"), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T(TKEY("actions"), "Actions"), ImGuiTableColumnFlags_WidthFixed, 60.0f * Util::GetUIScale()); auto& recordMarkers = settings.recordMarkers; @@ -1635,7 +1695,8 @@ void EditorWindow::ShowSettingsWindow() deleteActive.w = 1.0f; { auto styledButton = Util::StyledButtonWrapper(deleteColor, deleteHovered, deleteActive); - if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { + auto deleteLabel = std::format("{}##{}", T(TKEY("delete"), "Delete"), recordMarker.first); + if (ImGui::Button(deleteLabel.c_str(), ImVec2(-1, 0))) { markerToDelete = recordMarker.first; } } @@ -1680,7 +1741,7 @@ void EditorWindow::ShowSettingsWindow() ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - if (recordMarkers.size() < maxRecordMarkers && ImGui::Selectable("Add new marker")) { + if (recordMarkers.size() < maxRecordMarkers && ImGui::Selectable(T(TKEY("add_new_marker"), "Add new marker"))) { recordMarkers.insert({ std::format("New marker {}", recordMarkers.size()), { 0.5f, 0.5f, 0.5f, 1.0f } }); Save(); } @@ -1874,18 +1935,21 @@ void EditorWindow::DrawTimeControls() return; const float framePadX = ImGui::GetStyle().FramePadding.x * 2.0f; - const float buttonWidth = std::max({ ImGui::CalcTextSize("Resume Time").x, - ImGui::CalcTextSize("Pause Time").x, - ImGui::CalcTextSize("Reset Speed").x }) + + const char* resumeTimeText = T(TKEY("resume_time"), "Resume Time"); + const char* pauseTimeText = T(TKEY("pause_time"), "Pause Time"); + const char* resetSpeedText = T(TKEY("reset_speed"), "Reset Speed"); + const float buttonWidth = std::max({ ImGui::CalcTextSize(resumeTimeText).x, + ImGui::CalcTextSize(pauseTimeText).x, + ImGui::CalcTextSize(resetSpeedText).x }) + framePadX; - if (ImGui::Button(timePaused ? "Resume Time" : "Pause Time", ImVec2(buttonWidth, 0))) + if (ImGui::Button(timePaused ? resumeTimeText : pauseTimeText, ImVec2(buttonWidth, 0))) TogglePause(); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Pause or resume game time progression"); + ImGui::Text("%s", T(TKEY("pause_time_tooltip"), "Pause or resume game time progression")); ImGui::SameLine(); - DrawGameHourSlider(); + DrawGameHourSlider(T(TKEY("game_time"), "Game Time")); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Adjust the current game time"); + ImGui::Text("%s", T(TKEY("game_time_tooltip"), "Adjust the current game time")); // Sync slider with actual value if (timePaused) @@ -1894,22 +1958,22 @@ void EditorWindow::DrawTimeControls() timeScaleSlider = calendar->timeScale->value; // Row 2: Reset Speed + TimeScale slider + speed label - if (ImGui::Button("Reset Speed", ImVec2(buttonWidth, 0))) + if (ImGui::Button(resetSpeedText, ImVec2(buttonWidth, 0))) ResetTimeScale(); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Reset time speed to vanilla (%.1fx)", kVanillaTimeScale); + ImGui::Text(T(TKEY("reset_speed_tooltip"), "Reset time speed to vanilla (%.1fx)"), kVanillaTimeScale); ImGui::SameLine(); ImGui::BeginDisabled(timePaused); if (ImGui::SliderFloat("##TimeScale", &timeScaleSlider, kTimeScaleMin, kTimeScaleMax, - timeScaleSlider == kVanillaTimeScale ? "Vanilla Speed" : "", ImGuiSliderFlags_Logarithmic)) + timeScaleSlider == kVanillaTimeScale ? T(TKEY("vanilla_speed"), "Vanilla Speed") : "", ImGuiSliderFlags_Logarithmic)) calendar->timeScale->value = timeScaleSlider; ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Text("%.1fx", calendar->timeScale->value); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Adjust how fast time passes (vanilla: %.1fx)", kVanillaTimeScale); + ImGui::Text(T(TKEY("time_scale_tooltip"), "Adjust how fast time passes (vanilla: %.1fx)"), kVanillaTimeScale); } bool EditorWindow::CanBeOpen() @@ -2095,7 +2159,7 @@ void EditorWindow::PerformUndo() state.widget->LoadSettings(); state.widget->ApplyChanges(); ShowNotification( - std::format("Undone changes to {}", state.widgetId), + std::vformat(T(TKEY("undone_changes_to"), "Undone changes to {}"), std::make_format_args(state.widgetId)), Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, 2.0f); } @@ -2242,3 +2306,5 @@ bool EditorWindow::IsFavorite(const std::string& widgetId) const { return std::find(settings.favoriteWidgets.begin(), settings.favoriteWidgets.end(), widgetId) != settings.favoriteWidgets.end(); } + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/InteriorOnlyPanel.cpp b/src/CSEditor/InteriorOnlyPanel.cpp index 0c40d0d5d1..bd40c0f55d 100644 --- a/src/CSEditor/InteriorOnlyPanel.cpp +++ b/src/CSEditor/InteriorOnlyPanel.cpp @@ -1,12 +1,15 @@ #include "InteriorOnlyPanel.h" #include "../Globals.h" +#include "../I18n/I18n.h" #include "../Menu.h" #include "../Menu/ThemeManager.h" #include "../SceneSettingsManager.h" #include "../Utils/UI.h" #include "EditorWindow.h" +#define I18N_KEY_PREFIX "cs_editor." + namespace InteriorOnlyPanel { using SceneType = SceneSettingsManager::SceneType; @@ -52,7 +55,7 @@ namespace InteriorOnlyPanel if (cachedFeatureNames.empty()) cachedFeatureNames = SceneSettingsManager::GetInteriorRelevantFeatureNames(); - const char* featurePreview = (selectedFeatureIdx >= 0 && selectedFeatureIdx < static_cast(cachedFeatureNames.size())) ? cachedFeatureNames[selectedFeatureIdx].c_str() : "Select Feature..."; + const char* featurePreview = (selectedFeatureIdx >= 0 && selectedFeatureIdx < static_cast(cachedFeatureNames.size())) ? cachedFeatureNames[selectedFeatureIdx].c_str() : T(TKEY("select_feature"), "Select Feature..."); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * C::SCENE_FEATURE_DROPDOWN_RATIO); if (ImGui::BeginCombo("##FeatureSelect", featurePreview)) { @@ -75,7 +78,7 @@ namespace InteriorOnlyPanel { auto _ = Util::DisableGuard(selectedFeatureIdx < 0); - const char* settingPreview = (selectedSettingIdx >= 0 && selectedSettingIdx < static_cast(cachedSettingKeys.size())) ? cachedSettingKeys[selectedSettingIdx].c_str() : "Select Setting..."; + const char* settingPreview = (selectedSettingIdx >= 0 && selectedSettingIdx < static_cast(cachedSettingKeys.size())) ? cachedSettingKeys[selectedSettingIdx].c_str() : T(TKEY("select_setting"), "Select Setting..."); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * C::SCENE_SETTING_DROPDOWN_RATIO); if (ImGui::BeginCombo("##SettingSelect", settingPreview)) { @@ -105,7 +108,7 @@ namespace InteriorOnlyPanel bool canAdd = selectedFeatureIdx >= 0 && selectedSettingIdx >= 0; { auto _ = Util::DisableGuard(!canAdd); - if (ImGui::Button("Add")) { + if (ImGui::Button(T(TKEY("add"), "Add"))) { auto& featureName = cachedFeatureNames[selectedFeatureIdx]; auto& settingKey = cachedSettingKeys[selectedSettingIdx]; auto currentValue = SceneSettingsManager::GetFeatureSettingValue(featureName, settingKey); @@ -181,7 +184,7 @@ namespace InteriorOnlyPanel } break; default: - ImGui::TextDisabled("(unsupported type)"); + ImGui::TextDisabled("%s", T(TKEY("unsupported_type"), "(unsupported type)")); break; } @@ -194,7 +197,7 @@ namespace InteriorOnlyPanel if (Util::FeatureToggle("##active", &active)) manager->TogglePauseEntry(kSceneType, index); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text(entry.paused ? "Paused - click to resume" : "Active - click to pause"); + ImGui::Text("%s", entry.paused ? T(TKEY("paused_click_resume"), "Paused - click to resume") : T(TKEY("active_click_pause"), "Active - click to pause")); // Delete button ImGui::SameLine(); @@ -212,7 +215,7 @@ namespace InteriorOnlyPanel } } if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text(entry.source == EntrySource::Overwrite ? "Delete overwrite file from disk" : "Remove this setting"); + ImGui::Text("%s", entry.source == EntrySource::Overwrite ? T(TKEY("delete_overwrite_file"), "Delete overwrite file from disk") : T(TKEY("remove_setting"), "Remove this setting")); ImGui::PopID(); } @@ -224,7 +227,7 @@ namespace InteriorOnlyPanel auto& theme = globals::menu->GetSettings().Theme; // Header - ImGui::Text("Interior Only Settings"); + ImGui::Text("%s", T(TKEY("interior_only_settings"), "Interior Only Settings")); ImGui::Separator(); @@ -257,28 +260,29 @@ namespace InteriorOnlyPanel if (entries.empty()) { ImGui::Spacing(); ImGui::TextColored(theme.StatusPalette.Disable, - "No interior-only settings configured."); + "%s", T(TKEY("no_interior_settings"), "No interior-only settings configured.")); ImGui::TextColored(theme.StatusPalette.Disable, - "Click + to add settings that will only apply in interiors."); + "%s", T(TKEY("click_plus_add"), "Click + to add settings that will only apply in interiors.")); ImGui::Spacing(); - ImGui::TextWrapped( - "Settings added here will override feature defaults when you enter an interior cell. " - "Values revert automatically when you exit."); + ImGui::TextWrapped("%s", + T(TKEY("interior_settings_override"), + "Settings added here will override feature defaults when you enter an interior cell. " + "Values revert automatically when you exit.")); return; } // --- Overwrite Files Section --- if (!overwriteIndices.empty()) { ImGui::Spacing(); - ImGui::TextColored(theme.StatusPalette.InfoColor, "Overwrite Files"); + ImGui::TextColored(theme.StatusPalette.InfoColor, "%s", T(TKEY("overwrite_files"), "Overwrite Files")); ImGui::SameLine(); bool allPaused = manager->AreAllOverwritesPaused(kSceneType); - if (ImGui::SmallButton(allPaused ? "Unpause All" : "Pause All")) + if (ImGui::SmallButton(allPaused ? T(TKEY("unpause_all"), "Unpause All") : T(TKEY("pause_all"), "Pause All"))) manager->SetAllOverwritesPaused(kSceneType, !allPaused); ImGui::SameLine(); - if (ImGui::SmallButton("Delete All")) + if (ImGui::SmallButton(T(TKEY("delete_all"), "Delete All"))) deleteAllOverwritesPopup.Request(); ImGui::Separator(); @@ -291,16 +295,18 @@ namespace InteriorOnlyPanel if (!userIndices.empty()) { if (!overwriteIndices.empty()) { ImGui::Spacing(); - ImGui::TextColored(theme.FeatureHeading.ColorDefault, "User Settings"); + ImGui::TextColored(theme.FeatureHeading.ColorDefault, "%s", T(TKEY("user_settings"), "User Settings")); ImGui::SameLine(); } bool allUserPaused = manager->AreAllUserPaused(kSceneType); - if (ImGui::SmallButton(allUserPaused ? "Unpause All##user" : "Pause All##user")) + auto pauseAllLabel = std::format("{}##user", allUserPaused ? T(TKEY("unpause_all"), "Unpause All") : T(TKEY("pause_all"), "Pause All")); + if (ImGui::SmallButton(pauseAllLabel.c_str())) manager->SetAllUserPaused(kSceneType, !allUserPaused); ImGui::SameLine(); - if (ImGui::SmallButton("Delete All##user")) + auto deleteAllLabel = std::format("{}##user", T(TKEY("delete_all"), "Delete All")); + if (ImGui::SmallButton(deleteAllLabel.c_str())) deleteAllUserPopup.Request(); if (!overwriteIndices.empty()) @@ -315,3 +321,5 @@ namespace InteriorOnlyPanel } } } + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/LightEditor.cpp b/src/CSEditor/LightEditor.cpp index f405f53568..8c6f5fa899 100644 --- a/src/CSEditor/LightEditor.cpp +++ b/src/CSEditor/LightEditor.cpp @@ -1,8 +1,11 @@ #include "LightEditor.h" #include "../Features/InverseSquareLighting.h" #include "../Features/LightLimitFix.h" +#include "../I18n/I18n.h" #include "../Menu.h" +#define I18N_KEY_PREFIX "feature.light_editor." + #include #include #include @@ -10,32 +13,32 @@ void LightEditor::DrawSettings() { - ImGui::Checkbox("Disable Regular Falloff Lights", &disableRegularLights); - ImGui::Checkbox("Disable Inverse Square Falloff Lights", &disableInvSqLights); + ImGui::Checkbox(T(TKEY("disable_regular_falloff_lights"), "Disable Regular Falloff Lights"), &disableRegularLights); + ImGui::Checkbox(T(TKEY("disable_inverse_square_falloff_lights"), "Disable Inverse Square Falloff Lights"), &disableInvSqLights); ImGui::Spacing(); - ImGui::Text("Total Lights: %u", totalLightCount); - ImGui::Text("Active Shadow Lights: %u", activeShadowLightCount); + ImGui::Text(T(TKEY("total_lights"), "Total Lights: %u"), totalLightCount); + ImGui::Text(T(TKEY("active_shadow_lights"), "Active Shadow Lights: %u"), activeShadowLightCount); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::Checkbox("Shadows Only", &shadowsOnly); + ImGui::Checkbox(T(TKEY("shadows_only"), "Shadows Only"), &shadowsOnly); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Only show lights with HemiShadow or OmniShadow flags."); + ImGui::Text("%s", T(TKEY("shadows_only_tooltip"), "Only show lights with HemiShadow or OmniShadow flags.")); } int selectedFilter = static_cast(filterOption); - if (ImGui::Combo("Filter By", &selectedFilter, FilterOptionLabels, static_cast(FilterOption::Count))) { + if (ImGui::Combo(T(TKEY("filter_by"), "Filter By"), &selectedFilter, FilterOptionLabels, static_cast(FilterOption::Count))) { filterOption = static_cast(selectedFilter); } int selectedSort = static_cast(sortOption); - if (ImGui::Combo("Sort By", &selectedSort, SortOptionLabels, static_cast(SortOption::Count))) { + if (ImGui::Combo(T(TKEY("sort_by"), "Sort By"), &selectedSort, SortOptionLabels, static_cast(SortOption::Count))) { sortOption = static_cast(selectedSort); } - if (ImGui::BeginCombo("Lights", selected.isSelected ? GetLightName(selected).c_str() : "Select a light")) { + if (ImGui::BeginCombo(T(TKEY("lights"), "Lights"), selected.isSelected ? GetLightName(selected).c_str() : T(TKEY("select_a_light"), "Select a light"))) { for (auto& light : lights) { const auto displayName = GetLightName(light); const bool isSelected = light == selected; @@ -57,21 +60,21 @@ void LightEditor::DrawSettings() return; if (selected.isRef || selected.isAttached) { - ImGui::Text("Owner: 0x%08X | %s", selected.id, displayInfo.ownerEditorId.c_str()); - ImGui::Text("Owner last edited by: %s", displayInfo.ownerLastEditedBy.c_str()); - ImGui::Text("Base Object: 0x%08X | %s", displayInfo.baseObjectFormId, selected.name.c_str()); - ImGui::Text("LIGH: 0x%08X | %s", displayInfo.lighFormId, displayInfo.lighEditorId.c_str()); - ImGui::Text("Cell: %s", displayInfo.cellEditorId.c_str()); + ImGui::Text(T(TKEY("owner"), "Owner: 0x%08X | %s"), selected.id, displayInfo.ownerEditorId.c_str()); + ImGui::Text(T(TKEY("owner_last_edited_by"), "Owner last edited by: %s"), displayInfo.ownerLastEditedBy.c_str()); + ImGui::Text(T(TKEY("base_object"), "Base Object: 0x%08X | %s"), displayInfo.baseObjectFormId, selected.name.c_str()); + ImGui::Text(T(TKEY("ligh"), "LIGH: 0x%08X | %s"), displayInfo.lighFormId, displayInfo.lighEditorId.c_str()); + ImGui::Text(T(TKEY("cell"), "Cell: %s"), displayInfo.cellEditorId.c_str()); } else { - ImGui::Text("Memory Address: %p", selected.ptr); - ImGui::Text("NiLight Name: %s", selected.name.c_str()); + ImGui::Text(T(TKEY("memory_address"), "Memory Address: %p"), selected.ptr); + ImGui::Text(T(TKEY("ni_light_name"), "NiLight Name: %s"), selected.name.c_str()); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("Revert Changes")) { + if (ImGui::Button(T(TKEY("revert_changes"), "Revert Changes"))) { current = original; current.pos = { 0, 0, 0 }; waitFrames = 1; @@ -79,11 +82,11 @@ void LightEditor::DrawSettings() if (lpInfo.isLPLight) { ImGui::SameLine(); - if (ImGui::Button("Save to Light Placer")) { + if (ImGui::Button(T(TKEY("save_to_light_placer"), "Save to Light Placer"))) { SaveToLightPlacer(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Save current settings to the Light Placer JSON."); + ImGui::Text("%s", T(TKEY("save_to_light_placer_tooltip"), "Save current settings to the Light Placer JSON.")); } } @@ -91,57 +94,59 @@ void LightEditor::DrawSettings() ImGui::Spacing(); if (selected.isSpotlight) - ImGui::TextDisabled("Spotlight: ISL light type flags not applicable"); + ImGui::TextDisabled("%s", T(TKEY("spotlight_not_applicable"), "Spotlight: ISL light type flags not applicable")); ImGui::BeginDisabled(selected.isSpotlight); - ImGui::CheckboxFlags("Inverse Square Light", reinterpret_cast(¤t.data.flags), static_cast(LightLimitFix::LightFlags::InverseSquare)); + ImGui::CheckboxFlags(T(TKEY("inverse_square_light"), "Inverse Square Light"), reinterpret_cast(¤t.data.flags), static_cast(LightLimitFix::LightFlags::InverseSquare)); ImGui::EndDisabled(); - ImGui::CheckboxFlags("Linear Light", reinterpret_cast(¤t.data.flags), static_cast(LightLimitFix::LightFlags::Linear)); + ImGui::CheckboxFlags(T(TKEY("linear_light"), "Linear Light"), reinterpret_cast(¤t.data.flags), static_cast(LightLimitFix::LightFlags::Linear)); ImGui::Spacing(); ImGui::Spacing(); - ImGui::ColorEdit3("Color", ¤t.data.diffuse.red); - ImGui::SliderFloat("Intensity", ¤t.data.fade, 0.01f, 16.f, "%.3f"); + ImGui::ColorEdit3(T(TKEY("color"), "Color"), ¤t.data.diffuse.red); + ImGui::SliderFloat(T(TKEY("intensity"), "Intensity"), ¤t.data.fade, 0.01f, 16.f, "%.3f"); const auto isInvSq = current.data.flags.any(LightLimitFix::LightFlags::InverseSquare); if (isInvSq) ImGui::BeginDisabled(); - ImGui::SliderFloat("Radius", ¤t.data.radius, 2.f, 8096.f, "%.0f"); + ImGui::SliderFloat(T(TKEY("radius"), "Radius"), ¤t.data.radius, 2.f, 8096.f, "%.0f"); if (isInvSq) ImGui::EndDisabled(); if (isInvSq) { - ImGui::SliderFloat("Size", ¤t.data.size, 0.01f, 10.0f, "%.3f"); - ImGui::SliderFloat("Cutoff", ¤t.data.cutoffOverride, 0.01f, 1.f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("size"), "Size"), ¤t.data.size, 0.01f, 10.0f, "%.3f"); + ImGui::SliderFloat(T(TKEY("cutoff"), "Cutoff"), ¤t.data.cutoffOverride, 0.01f, 1.f, "%.3f", ImGuiSliderFlags_AlwaysClamp); } ImGui::Spacing(); ImGui::Spacing(); if (!selected.isOther && current.data.lighFormId != 0 && selected.hasPosition) { - ImGui::Text("X: %.2f, Y: %.2f, Z: %.2f", displayInfo.pos.x, displayInfo.pos.y, displayInfo.pos.z); + ImGui::Text(T(TKEY("position_format"), "X: %.2f, Y: %.2f, Z: %.2f"), displayInfo.pos.x, displayInfo.pos.y, displayInfo.pos.z); ImGui::Spacing(); - ImGui::SliderFloat3("Position Offset", ¤t.pos.x, -500.f, 500.f, "%.0f"); + ImGui::SliderFloat3(T(TKEY("position_offset"), "Position Offset"), ¤t.pos.x, -500.f, 500.f, "%.0f"); ImGui::Spacing(); ImGui::Spacing(); auto* flags = reinterpret_cast(¤t.tesFlags); ImGui::Spacing(); - ImGui::Text("Light Flags"); - ImGui::CheckboxFlags("Dynamic", flags, static_cast(RE::TES_LIGHT_FLAGS::kDynamic)); - ImGui::CheckboxFlags("Negative", flags, static_cast(RE::TES_LIGHT_FLAGS::kNegative)); - ImGui::CheckboxFlags("Flicker", flags, static_cast(RE::TES_LIGHT_FLAGS::kFlicker)); - ImGui::CheckboxFlags("Flicker Slow", flags, static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)); - ImGui::CheckboxFlags("Pulse", flags, static_cast(RE::TES_LIGHT_FLAGS::kPulse)); - ImGui::CheckboxFlags("Pulse Slow", flags, static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)); - ImGui::CheckboxFlags("Hemi Shadow", flags, static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)); - ImGui::CheckboxFlags("Omni Shadow", flags, static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)); - ImGui::CheckboxFlags("Portal Strict", flags, static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)); + ImGui::Text("%s", T(TKEY("light_flags"), "Light Flags")); + ImGui::CheckboxFlags(T(TKEY("dynamic"), "Dynamic"), flags, static_cast(RE::TES_LIGHT_FLAGS::kDynamic)); + ImGui::CheckboxFlags(T(TKEY("negative"), "Negative"), flags, static_cast(RE::TES_LIGHT_FLAGS::kNegative)); + ImGui::CheckboxFlags(T(TKEY("flicker"), "Flicker"), flags, static_cast(RE::TES_LIGHT_FLAGS::kFlicker)); + ImGui::CheckboxFlags(T(TKEY("flicker_slow"), "Flicker Slow"), flags, static_cast(RE::TES_LIGHT_FLAGS::kFlickerSlow)); + ImGui::CheckboxFlags(T(TKEY("pulse"), "Pulse"), flags, static_cast(RE::TES_LIGHT_FLAGS::kPulse)); + ImGui::CheckboxFlags(T(TKEY("pulse_slow"), "Pulse Slow"), flags, static_cast(RE::TES_LIGHT_FLAGS::kPulseSlow)); + ImGui::CheckboxFlags(T(TKEY("hemi_shadow"), "Hemi Shadow"), flags, static_cast(RE::TES_LIGHT_FLAGS::kHemiShadow)); + ImGui::CheckboxFlags(T(TKEY("omni_shadow"), "Omni Shadow"), flags, static_cast(RE::TES_LIGHT_FLAGS::kOmniShadow)); + ImGui::CheckboxFlags(T(TKEY("portal_strict"), "Portal Strict"), flags, static_cast(RE::TES_LIGHT_FLAGS::kPortalStrict)); } } +#undef I18N_KEY_PREFIX + std::string LightEditor::GetLightName(LightInfo& lightInfo) { if (lightInfo.isRef) diff --git a/src/CSEditor/PaletteWindow.cpp b/src/CSEditor/PaletteWindow.cpp index 905e3ff4c2..7c08c79982 100644 --- a/src/CSEditor/PaletteWindow.cpp +++ b/src/CSEditor/PaletteWindow.cpp @@ -1,8 +1,13 @@ #include "PaletteWindow.h" +#include "../I18n/I18n.h" #include "EditorWindow.h" #include "Menu/ThemeManager.h" #include "Utils/UI.h" +#include + +#define I18N_KEY_PREFIX "cs_editor." + // Forward declaration from EditorWindow.cpp void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled); @@ -24,14 +29,14 @@ void PaletteWindow::Draw() ImGui::SetNextWindowPos( ImVec2(displaySize.x - paletteWidth - pad, bottomY - paletteHeight), layoutCond); - if (Util::BeginWithRoundedClose("Palette", &open, ImGuiWindowFlags_NoFocusOnAppearing)) { + if (Util::BeginWithRoundedClose(T(TKEY("palette"), "Palette"), &open, ImGuiWindowFlags_NoFocusOnAppearing)) { if (ImGui::BeginTabBar("PaletteTabs")) { - if (ImGui::BeginTabItem("Colours")) { + if (ImGui::BeginTabItem(T(TKEY("colours"), "Colours"))) { DrawColorsTab(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Values")) { + if (ImGui::BeginTabItem(T(TKEY("values"), "Values"))) { DrawValuesTab(); ImGui::EndTabItem(); } @@ -49,8 +54,8 @@ void PaletteWindow::DrawColorsTab() const float spacing = 8.0f * scale; // Favorites section at top - ImGui::SeparatorText("Favourites"); - ImGui::TextWrapped("Drag colours here to save as favourites."); + ImGui::SeparatorText(T(TKEY("favourites"), "Favourites")); + ImGui::TextWrapped("%s", T(TKEY("drag_colours_here"), "Drag colours here to save as favourites.")); ImGui::Spacing(); for (int i = 0; i < maxFavoriteSlots; i++) { @@ -79,14 +84,20 @@ void PaletteWindow::DrawColorsTab() // Right-click to clear if (ImGui::BeginPopupContextItem()) { - if (ImGui::Selectable("Clear favourite")) { + if (ImGui::Selectable(T(TKEY("clear_favourite"), "Clear favourite"))) { favoriteColors[i].reset(); Save(); } ImGui::EndPopup(); } - Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\nClick to copy\nRight-click to clear", color.x, color.y, color.z).c_str()); + Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\n{}\n{}", + color.x, + color.y, + color.z, + T(TKEY("click_to_copy"), "Click to copy"), + T(TKEY("right_click_to_clear"), "Right-click to clear")) + .c_str()); } else { // Show empty favorite slot with star ImVec4 emptyColor(0.2f, 0.2f, 0.2f, 1.0f); @@ -103,7 +114,7 @@ void PaletteWindow::DrawColorsTab() DrawIconStar(center, starSize, starColor, false); - Util::AddTooltip("Drag a colour here to add to favourites"); + Util::AddTooltip(T(TKEY("drag_to_favourites"), "Drag a colour here to add to favourites")); } // Drag-and-drop target @@ -121,11 +132,11 @@ void PaletteWindow::DrawColorsTab() ImGui::Spacing(); // Recently Used section - ImGui::SeparatorText("Recently Used"); + ImGui::SeparatorText(T(TKEY("recently_used"), "Recently Used")); auto recentColors = GetRecentColors(5); if (recentColors.empty()) { - ImGui::TextDisabled("No recent colors"); + ImGui::TextDisabled("%s", T(TKEY("no_recent_colors"), "No recent colors")); } else { for (size_t i = 0; i < recentColors.size(); i++) { if (i > 0) @@ -148,8 +159,10 @@ void PaletteWindow::DrawColorsTab() ImGui::EndDragDropSource(); } - Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\nUsed {} times\nClick to copy", - entry->color.x, entry->color.y, entry->color.z, entry->useCount) + Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\n{}\n{}", + entry->color.x, entry->color.y, entry->color.z, + std::vformat(T(TKEY("used_times"), "Used {} times"), std::make_format_args(entry->useCount)), + T(TKEY("click_to_copy"), "Click to copy")) .c_str()); } } @@ -158,16 +171,16 @@ void PaletteWindow::DrawColorsTab() // Most Used section ImGui::Separator(); ImGui::Spacing(); - ImGui::TextUnformatted("Most Used"); + ImGui::TextUnformatted(T(TKEY("most_used"), "Most Used")); ImGui::Spacing(); - ImGui::TextWrapped("Favourite/most commonly used colours here."); + ImGui::TextWrapped("%s", T(TKEY("fav_most_colours"), "Favourite/most commonly used colours here.")); ImGui::Spacing(); auto mostUsedColors = GetMostUsedColors(20); if (mostUsedColors.empty()) { - ImGui::TextDisabled("No frequently used colors yet"); - ImGui::TextDisabled("(Colors used 3+ times will appear here)"); + ImGui::TextDisabled("%s", T(TKEY("no_frequent_colors"), "No frequently used colors yet")); + ImGui::TextDisabled("%s", T(TKEY("colors_3_plus"), "(Colors used 3+ times will appear here)")); } else { int colorIndex = 0; for (auto* entry : mostUsedColors) { @@ -192,7 +205,7 @@ void PaletteWindow::DrawColorsTab() // Right-click to remove if (ImGui::BeginPopupContextItem()) { - if (ImGui::Selectable("Remove from palette")) { + if (ImGui::Selectable(T(TKEY("remove_from_palette"), "Remove from palette"))) { auto it = std::find_if(colorEntries.begin(), colorEntries.end(), [entry](const ColorEntry& e) { return &e == entry; }); if (it != colorEntries.end()) { @@ -203,8 +216,11 @@ void PaletteWindow::DrawColorsTab() ImGui::EndPopup(); } - Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\nUsed {} times\nClick to copy\nRight-click to remove", - entry->color.x, entry->color.y, entry->color.z, entry->useCount) + Util::AddTooltip(std::format("RGB: {:.3f}, {:.3f}, {:.3f}\n{}\n{}\n{}", + entry->color.x, entry->color.y, entry->color.z, + std::vformat(T(TKEY("used_times"), "Used {} times"), std::make_format_args(entry->useCount)), + T(TKEY("click_to_copy"), "Click to copy"), + T(TKEY("right_click_to_remove"), "Right-click to remove")) .c_str()); colorIndex++; @@ -215,10 +231,10 @@ void PaletteWindow::DrawColorsTab() void PaletteWindow::DrawValuesTab() { // Recently Used section - ImGui::SeparatorText("Recently Used"); + ImGui::SeparatorText(T(TKEY("recently_used"), "Recently Used")); auto recentValues = GetRecentValues(3); if (recentValues.empty()) { - ImGui::TextDisabled("No recent values"); + ImGui::TextDisabled("%s", T(TKEY("no_recent_values"), "No recent values")); } else { for (auto* entry : recentValues) { std::string label = std::format("{}: {:.3f}", entry->name, entry->value); @@ -229,23 +245,26 @@ void PaletteWindow::DrawValuesTab() ImGui::SetClipboardText(std::to_string(entry->value).c_str()); } - Util::AddTooltip(std::format("Used {} times\nClick to copy", entry->useCount).c_str()); + Util::AddTooltip(std::format("{}\n{}", + std::vformat(T(TKEY("used_times"), "Used {} times"), std::make_format_args(entry->useCount)), + T(TKEY("click_to_copy"), "Click to copy")) + .c_str()); } } ImGui::Spacing(); // Most Used section ImGui::Separator(); ImGui::Spacing(); - ImGui::TextUnformatted("Most Used"); + ImGui::TextUnformatted(T(TKEY("most_used"), "Most Used")); ImGui::Spacing(); - ImGui::TextWrapped("Favourite/most commonly used values here."); + ImGui::TextWrapped("%s", T(TKEY("fav_most_values"), "Favourite/most commonly used values here.")); ImGui::Spacing(); auto mostUsedValues = GetMostUsedValues(20); if (mostUsedValues.empty()) { - ImGui::TextDisabled("No frequently used values yet"); - ImGui::TextDisabled("(Values used 3+ times will appear here)"); + ImGui::TextDisabled("%s", T(TKEY("no_frequent_values"), "No frequently used values yet")); + ImGui::TextDisabled("%s", T(TKEY("values_3_plus"), "(Values used 3+ times will appear here)")); } else { for (auto* entry : mostUsedValues) { std::string label = std::format("{}: {:.3f}##{}", entry->name, entry->value, (void*)entry); @@ -258,7 +277,7 @@ void PaletteWindow::DrawValuesTab() // Right-click to remove if (ImGui::BeginPopupContextItem()) { - if (ImGui::Selectable("Remove from palette")) { + if (ImGui::Selectable(T(TKEY("remove_from_palette"), "Remove from palette"))) { auto it = std::find_if(valueEntries.begin(), valueEntries.end(), [entry](const ValueEntry& e) { return &e == entry; }); if (it != valueEntries.end()) { @@ -269,7 +288,11 @@ void PaletteWindow::DrawValuesTab() ImGui::EndPopup(); } - Util::AddTooltip(std::format("Used {} times\nClick to copy\nRight-click to remove", entry->useCount).c_str()); + Util::AddTooltip(std::format("{}\n{}\n{}", + std::vformat(T(TKEY("used_times"), "Used {} times"), std::make_format_args(entry->useCount)), + T(TKEY("click_to_copy"), "Click to copy"), + T(TKEY("right_click_to_remove"), "Right-click to remove")) + .c_str()); } } } @@ -497,3 +520,5 @@ void PaletteWindow::Load() valueEntries.push_back(entry); } } + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/Weather/CellLightingWidget.cpp b/src/CSEditor/Weather/CellLightingWidget.cpp index f784653f32..2c2ad62727 100644 --- a/src/CSEditor/Weather/CellLightingWidget.cpp +++ b/src/CSEditor/Weather/CellLightingWidget.cpp @@ -1,8 +1,11 @@ #include "CellLightingWidget.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "../WeatherUtils.h" #include "Utils/UI.h" +#define I18N_KEY_PREFIX "cs_editor." + namespace { namespace CellLightingTab @@ -60,10 +63,10 @@ void CellLightingWidget::DrawWidget() } if (!cell || !cell->IsInteriorCell()) { - Util::Text::Warning("This cell is not an interior cell."); - ImGui::TextWrapped("Cell lighting properties only apply to interior cells."); + Util::Text::Warning("%s", T(TKEY("not_interior_cell"), "This cell is not an interior cell.")); + ImGui::TextWrapped("%s", T(TKEY("cell_lighting_interior_only"), "Cell Lighting is only available for interior cells.")); } else if (!cell->GetLighting()) { - Util::Text::Error("No lighting data available for this cell."); + Util::Text::Error("%s", T(TKEY("no_lighting_data"), "No lighting data available for this cell.")); } else { bool changed = false; @@ -74,16 +77,17 @@ void CellLightingWidget::DrawWidget() const ImGuiTabItemFlags inheritFlags = GetTabFlagsForOverride(CellLightingTab::kInheritance); auto drawInherited = [](bool inherited, auto draw) -> bool { - if (inherited) PushInheritedStyle(); + if (inherited) + PushInheritedStyle(); const bool result = draw(); if (inherited) { - Util::AddTooltip("Inherited from lighting template"); + Util::AddTooltip(T(TKEY("inherited_from_lighting_template"), "Inherited from lighting template")); PopInheritedStyle(); } return result; }; - if (ImGui::BeginTabItem(CellLightingTab::kBasic, nullptr, basicFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_basic"), "Basic"), nullptr, basicFlags)) { BeginScrollableContent("##BasicScroll"); auto drawMatchedHeader = [&](bool matches, const char* label, auto draw) { @@ -98,7 +102,7 @@ void CellLightingWidget::DrawWidget() } }; - drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kAmbientColor, CellLightingSetting::kDirectionalColor }), "Ambient & Directional", [&]() { + drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kAmbientColor, CellLightingSetting::kDirectionalColor }), T(TKEY("ambient_directional"), "Ambient & Directional"), [&]() { changed |= drawInherited(settings.inheritAmbientColor, [&]() { return WeatherUtils::DrawColorEdit(CellLightingSetting::kAmbientColor, settings.ambient); }); @@ -108,27 +112,27 @@ void CellLightingWidget::DrawWidget() }); }); - drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kXYRotation, CellLightingSetting::kZRotation, CellLightingSetting::kDirectionalFade }), "Directional Settings", [&]() { + drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kXYRotation, CellLightingSetting::kZRotation, CellLightingSetting::kDirectionalFade }), T(TKEY("directional_settings"), "Directional Settings"), [&]() { int xyDegrees = settings.directionalXY; int zDegrees = settings.directionalZ; if (DrawIfMatchesSearch(CellLightingSetting::kXYRotation, [&](const char* label) { - return drawInherited(settings.inheritDirectionalRotation, [&]() { - return DrawWithHighlight(label, [&]() { - return ImGui::SliderInt(label, &xyDegrees, 0, 360); - }); - }); - })) { + return drawInherited(settings.inheritDirectionalRotation, [&]() { + return DrawWithHighlight(label, [&]() { + return ImGui::SliderInt(std::format("{}##{}", T(TKEY("xy_rotation"), "XY Rotation"), CellLightingSetting::kXYRotation).c_str(), &xyDegrees, 0, 360); + }); + }); + })) { settings.directionalXY = static_cast(xyDegrees); changed = true; } ImGui::Spacing(); if (DrawIfMatchesSearch(CellLightingSetting::kZRotation, [&](const char* label) { - return drawInherited(settings.inheritDirectionalRotation, [&]() { - return DrawWithHighlight(label, [&]() { - return ImGui::SliderInt(label, &zDegrees, 0, 360); - }); - }); - })) { + return drawInherited(settings.inheritDirectionalRotation, [&]() { + return DrawWithHighlight(label, [&]() { + return ImGui::SliderInt(std::format("{}##{}", T(TKEY("z_rotation"), "Z Rotation"), CellLightingSetting::kZRotation).c_str(), &zDegrees, 0, 360); + }); + }); + })) { settings.directionalZ = static_cast(zDegrees); changed = true; } @@ -138,7 +142,7 @@ void CellLightingWidget::DrawWidget() }); }); - drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kLightFadeStart, CellLightingSetting::kLightFadeEnd }), "Light Fade", [&]() { + drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kLightFadeStart, CellLightingSetting::kLightFadeEnd }), T(TKEY("light_fade"), "Light Fade"), [&]() { changed |= drawInherited(settings.inheritLightFadeDistances, [&]() { return WeatherUtils::DrawSliderFloat(CellLightingSetting::kLightFadeStart, settings.lightFadeStart, 0.0f, 163840.0f); }); @@ -148,7 +152,7 @@ void CellLightingWidget::DrawWidget() }); }); - drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kClipDistance }), "Other", [&]() { + drawMatchedHeader(MatchesAnySearch({ CellLightingSetting::kClipDistance }), T(TKEY("other"), "Other"), [&]() { changed |= drawInherited(settings.inheritClipDistance, [&]() { return WeatherUtils::DrawSliderFloat(CellLightingSetting::kClipDistance, settings.clipDist, 0.0f, 163840.0f); }); @@ -158,7 +162,7 @@ void CellLightingWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(CellLightingTab::kFog, nullptr, fogFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_fog"), "Fog"), nullptr, fogFlags)) { BeginScrollableContent("##FogScroll"); DrawSearchSectionIfMatches(CellLightingSetting::kFogNearColor, [&](const char*) { @@ -207,11 +211,11 @@ void CellLightingWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(CellLightingTab::kDalc, nullptr, dalcFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_dalc"), "DALC"), nullptr, dalcFlags)) { BeginScrollableContent("##DALCScroll"); if (MatchesAnySearch({ CellLightingSetting::kSpecular, CellLightingSetting::kFresnelPower })) { - ImGui::SeparatorText("Directional Ambient Lighting (DALC)"); + ImGui::SeparatorText(T(TKEY("dalc_header"), "Directional Ambient Lighting (DALC)")); changed |= drawInherited(settings.inheritAmbientColor, [&]() { return WeatherUtils::DrawColorEdit(CellLightingSetting::kSpecular, settings.directionalSpecular); }); @@ -222,7 +226,7 @@ void CellLightingWidget::DrawWidget() if (MatchesAnySearch({ CellLightingSetting::kXPlus, CellLightingSetting::kXMinus, CellLightingSetting::kYPlus, CellLightingSetting::kYMinus, CellLightingSetting::kZPlus, CellLightingSetting::kZMinus })) { - ImGui::SeparatorText("Directional Colors"); + ImGui::SeparatorText(T(TKEY("directional_colors"), "Directional Colors")); changed |= drawInherited(settings.inheritAmbientColor, [&]() { return WeatherUtils::DrawColorEdit(CellLightingSetting::kXPlus, settings.directionalXPlus); }); @@ -247,9 +251,9 @@ void CellLightingWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(CellLightingTab::kInheritance, nullptr, inheritFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_inheritance"), "Inheritance"), nullptr, inheritFlags)) { BeginScrollableContent("##InheritanceScroll"); - ImGui::TextWrapped("These flags control which lighting properties are inherited from the cell's lighting template."); + ImGui::TextWrapped("%s", T(TKEY("inherit_flags_desc"), "These flags control which lighting properties are inherited from the cell's lighting template.")); ImGui::Separator(); changed |= WeatherUtils::DrawCheckbox(CellLightingSetting::kInheritAmbientColor, settings.inheritAmbientColor); changed |= WeatherUtils::DrawCheckbox(CellLightingSetting::kInheritDirectionalColor, settings.inheritDirectionalColor); @@ -570,24 +574,26 @@ std::vector CellLightingWidget::CollectSearchableSettings( { const std::vector>> entries = { { CellLightingTab::kBasic, { CellLightingSetting::kAmbientColor, CellLightingSetting::kDirectionalColor, - CellLightingSetting::kXYRotation, CellLightingSetting::kZRotation, CellLightingSetting::kDirectionalFade, - CellLightingSetting::kLightFadeStart, CellLightingSetting::kLightFadeEnd, CellLightingSetting::kClipDistance } }, + CellLightingSetting::kXYRotation, CellLightingSetting::kZRotation, CellLightingSetting::kDirectionalFade, + CellLightingSetting::kLightFadeStart, CellLightingSetting::kLightFadeEnd, CellLightingSetting::kClipDistance } }, { CellLightingTab::kFog, { CellLightingSetting::kFogNearColor, CellLightingSetting::kFogFarColor, - CellLightingSetting::kFogNear, CellLightingSetting::kFogFar, CellLightingSetting::kFogPower, CellLightingSetting::kFogClampMax } }, + CellLightingSetting::kFogNear, CellLightingSetting::kFogFar, CellLightingSetting::kFogPower, CellLightingSetting::kFogClampMax } }, { CellLightingTab::kDalc, { CellLightingSetting::kSpecular, CellLightingSetting::kFresnelPower, - CellLightingSetting::kXPlus, CellLightingSetting::kXMinus, CellLightingSetting::kYPlus, CellLightingSetting::kYMinus, - CellLightingSetting::kZPlus, CellLightingSetting::kZMinus } }, + CellLightingSetting::kXPlus, CellLightingSetting::kXMinus, CellLightingSetting::kYPlus, CellLightingSetting::kYMinus, + CellLightingSetting::kZPlus, CellLightingSetting::kZMinus } }, { CellLightingTab::kInheritance, { CellLightingSetting::kInheritAmbientColor, CellLightingSetting::kInheritDirectionalColor, CellLightingSetting::kInheritFogColor, - CellLightingSetting::kInheritFogNear, CellLightingSetting::kInheritFogFar, CellLightingSetting::kInheritDirectionalRotation, - CellLightingSetting::kInheritDirectionalFade, CellLightingSetting::kInheritClipDistance, CellLightingSetting::kInheritFogPower, - CellLightingSetting::kInheritFogMaxClamp, CellLightingSetting::kInheritLightFadeDistances } }, + CellLightingSetting::kInheritFogNear, CellLightingSetting::kInheritFogFar, CellLightingSetting::kInheritDirectionalRotation, + CellLightingSetting::kInheritDirectionalFade, CellLightingSetting::kInheritClipDistance, CellLightingSetting::kInheritFogPower, + CellLightingSetting::kInheritFogMaxClamp, CellLightingSetting::kInheritLightFadeDistances } }, }; std::vector results; for (const auto& [tab, names] : entries) { for (const auto& name : names) { - results.push_back({ name, tab, name }); + results.push_back({ WeatherUtils::TranslateControlLabel(name), tab, name }); } } return results; } + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/Weather/LightingTemplateWidget.cpp b/src/CSEditor/Weather/LightingTemplateWidget.cpp index d74a41f457..ac9c49382c 100644 --- a/src/CSEditor/Weather/LightingTemplateWidget.cpp +++ b/src/CSEditor/Weather/LightingTemplateWidget.cpp @@ -1,8 +1,11 @@ #include "LightingTemplateWidget.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "../WeatherUtils.h" +#define I18N_KEY_PREFIX "cs_editor." + namespace { namespace LightingTemplateTab @@ -74,21 +77,21 @@ void LightingTemplateWidget::DrawWidget() const ImGuiTabItemFlags fogFlags = GetTabFlagsForOverride(LightingTemplateTab::kFog); const ImGuiTabItemFlags dalcFlags = GetTabFlagsForOverride(LightingTemplateTab::kDalc); - if (ImGui::BeginTabItem(LightingTemplateTab::kBasic, nullptr, basicFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_basic"), "Basic"), nullptr, basicFlags)) { BeginScrollableContent("##BasicScroll"); DrawBasicSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(LightingTemplateTab::kFog, nullptr, fogFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_fog"), "Fog"), nullptr, fogFlags)) { BeginScrollableContent("##FogScroll"); DrawFogSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(LightingTemplateTab::kDalc, nullptr, dalcFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_dalc"), "DALC"), nullptr, dalcFlags)) { BeginScrollableContent("##DALCScroll"); DrawDALCSettings(); EndScrollableContent(); @@ -115,13 +118,13 @@ void LightingTemplateWidget::DrawBasicSettings() } }; - drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kAmbientColor, LightingTemplateSetting::kDirectionalColor }), "Ambient & Directional", [&]() { + drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kAmbientColor, LightingTemplateSetting::kDirectionalColor }), T(TKEY("ambient_directional"), "Ambient & Directional"), [&]() { changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kAmbientColor, settings.ambient); ImGui::Spacing(); changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kDirectionalColor, settings.directional); }); - drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kDirectionalXY, LightingTemplateSetting::kDirectionalZ, LightingTemplateSetting::kDirectionalFade }), "Directional Settings", [&]() { + drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kDirectionalXY, LightingTemplateSetting::kDirectionalZ, LightingTemplateSetting::kDirectionalFade }), T(TKEY("directional_settings"), "Directional Settings"), [&]() { changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kDirectionalXY, settings.directionalXY, 0.0f, 360.0f); ImGui::Spacing(); changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kDirectionalZ, settings.directionalZ, 0.0f, 360.0f); @@ -129,13 +132,13 @@ void LightingTemplateWidget::DrawBasicSettings() changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kDirectionalFade, settings.directionalFade, 0.0f, 10.0f); }); - drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kLightFadeStart, LightingTemplateSetting::kLightFadeEnd }), "Light Fade", [&]() { + drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kLightFadeStart, LightingTemplateSetting::kLightFadeEnd }), T(TKEY("light_fade"), "Light Fade"), [&]() { changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kLightFadeStart, settings.lightFadeStart, 0.0f, 163840.0f); ImGui::Spacing(); changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kLightFadeEnd, settings.lightFadeEnd, 0.0f, 163840.0f); }); - drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kClipDistance }), "Other", [&]() { + drawMatchedHeader(MatchesAnySearch({ LightingTemplateSetting::kClipDistance }), T(TKEY("other"), "Other"), [&]() { changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kClipDistance, settings.clipDist, 0.0f, 163840.0f); }); @@ -188,14 +191,14 @@ void LightingTemplateWidget::DrawDALCSettings() bool changed = false; if (MatchesAnySearch({ LightingTemplateSetting::kSpecular, LightingTemplateSetting::kFresnelPower })) { - ImGui::SeparatorText("Directional Ambient Lighting (DALC)"); + ImGui::SeparatorText(T(TKEY("dalc_header"), "Directional Ambient Lighting (DALC)")); changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kSpecular, settings.dalc.specular); changed |= WeatherUtils::DrawSliderFloat(LightingTemplateSetting::kFresnelPower, settings.dalc.fresnelPower, 0.0f, 10.0f); } if (MatchesAnySearch({ LightingTemplateSetting::kXPlus, LightingTemplateSetting::kXMinus, LightingTemplateSetting::kYPlus, LightingTemplateSetting::kYMinus, LightingTemplateSetting::kZPlus, LightingTemplateSetting::kZMinus })) { - ImGui::SeparatorText("Directional Colors"); + ImGui::SeparatorText(T(TKEY("directional_colors"), "Directional Colors")); changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kXPlus, settings.dalc.directional[0].max); changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kXMinus, settings.dalc.directional[0].min); changed |= WeatherUtils::DrawColorEdit(LightingTemplateSetting::kYPlus, settings.dalc.directional[1].max); @@ -209,6 +212,8 @@ void LightingTemplateWidget::DrawDALCSettings() } } +#undef I18N_KEY_PREFIX + void LightingTemplateWidget::ApplyChanges() { SetLightingTemplateValues(); @@ -322,19 +327,19 @@ std::vector LightingTemplateWidget::CollectSearchableSetti { const std::vector>> entries = { { LightingTemplateTab::kBasic, { LightingTemplateSetting::kAmbientColor, LightingTemplateSetting::kDirectionalColor, - LightingTemplateSetting::kDirectionalXY, LightingTemplateSetting::kDirectionalZ, LightingTemplateSetting::kDirectionalFade, - LightingTemplateSetting::kLightFadeStart, LightingTemplateSetting::kLightFadeEnd, LightingTemplateSetting::kClipDistance } }, + LightingTemplateSetting::kDirectionalXY, LightingTemplateSetting::kDirectionalZ, LightingTemplateSetting::kDirectionalFade, + LightingTemplateSetting::kLightFadeStart, LightingTemplateSetting::kLightFadeEnd, LightingTemplateSetting::kClipDistance } }, { LightingTemplateTab::kFog, { LightingTemplateSetting::kFogColorNear, LightingTemplateSetting::kFogColorFar, - LightingTemplateSetting::kFogNear, LightingTemplateSetting::kFogFar, LightingTemplateSetting::kFogPower, LightingTemplateSetting::kFogClamp } }, + LightingTemplateSetting::kFogNear, LightingTemplateSetting::kFogFar, LightingTemplateSetting::kFogPower, LightingTemplateSetting::kFogClamp } }, { LightingTemplateTab::kDalc, { LightingTemplateSetting::kSpecular, LightingTemplateSetting::kFresnelPower, - LightingTemplateSetting::kXPlus, LightingTemplateSetting::kXMinus, LightingTemplateSetting::kYPlus, LightingTemplateSetting::kYMinus, - LightingTemplateSetting::kZPlus, LightingTemplateSetting::kZMinus } }, + LightingTemplateSetting::kXPlus, LightingTemplateSetting::kXMinus, LightingTemplateSetting::kYPlus, LightingTemplateSetting::kYMinus, + LightingTemplateSetting::kZPlus, LightingTemplateSetting::kZMinus } }, }; std::vector results; for (const auto& [tab, names] : entries) { for (const auto& name : names) { - results.push_back({ name, tab, name }); + results.push_back({ WeatherUtils::TranslateControlLabel(name), tab, name }); } } return results; diff --git a/src/CSEditor/Weather/PrecipitationWidget.cpp b/src/CSEditor/Weather/PrecipitationWidget.cpp index ebd730acf4..8efa1d915a 100644 --- a/src/CSEditor/Weather/PrecipitationWidget.cpp +++ b/src/CSEditor/Weather/PrecipitationWidget.cpp @@ -1,4 +1,5 @@ #include "PrecipitationWidget.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "../WeatherUtils.h" #include "Globals.h" @@ -6,6 +7,10 @@ #include "RE/N/NiSourceTexture.h" #include "Utils/Game.h" +#include + +#define I18N_KEY_PREFIX "cs_editor." + namespace { namespace PrecipitationTab @@ -47,29 +52,29 @@ void PrecipitationWidget::DrawWidget() const ImGuiTabItemFlags positionFlags = GetTabFlagsForOverride(PrecipitationTab::kPosition); const ImGuiTabItemFlags textureFlags = GetTabFlagsForOverride(PrecipitationTab::kTexture); - if (ImGui::BeginTabItem(PrecipitationTab::kParticle, nullptr, particleFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_particle"), "Particle"), nullptr, particleFlags)) { BeginScrollableContent("##ParticleScroll"); if (DrawIfMatchesSearch(PrecipitationSetting::kType, [&](const char* label) { - ImGui::SeparatorText("Particle Type"); - const char* types[] = { "Rain", "Snow" }; - int currentType = static_cast(settings.particleType); - bool comboChanged = DrawWithHighlight(label, [&]() { - return ImGui::Combo(label, ¤tType, types, IM_ARRAYSIZE(types)); - }); - if (comboChanged) { - settings.particleType = static_cast(currentType); - return true; - } - return false; - })) + ImGui::SeparatorText(T(TKEY("particle_type"), "Particle Type")); + const char* types[] = { T(TKEY("rain"), "Rain"), T(TKEY("snow"), "Snow") }; + int currentType = static_cast(settings.particleType); + bool comboChanged = DrawWithHighlight(label, [&]() { + return ImGui::Combo(std::format("{}##{}", T(TKEY("type"), "Type"), PrecipitationSetting::kType).c_str(), ¤tType, types, IM_ARRAYSIZE(types)); + }); + if (comboChanged) { + settings.particleType = static_cast(currentType); + return true; + } + return false; + })) changed = true; if (MatchesAnySearch({ PrecipitationSetting::kSizeX, PrecipitationSetting::kSizeY })) { - ImGui::SeparatorText("Particle Size"); + ImGui::SeparatorText(T(TKEY("particle_size"), "Particle Size")); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kSizeX, settings.particleSizeX, 0.0f, 200.0f); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kSizeY, settings.particleSizeY, 0.0f, 200.0f); } if (MatchesAnySearch({ PrecipitationSetting::kGravityVelocity, PrecipitationSetting::kRotationVelocity })) { - ImGui::SeparatorText("Velocity"); + ImGui::SeparatorText(T(TKEY("velocity"), "Velocity")); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kGravityVelocity, settings.gravityVelocity, 0.0f, 10000.0f); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kRotationVelocity, settings.rotationVelocity, 0.0f, 10000.0f); } @@ -77,16 +82,16 @@ void PrecipitationWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(PrecipitationTab::kPosition, nullptr, positionFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_position"), "Position"), nullptr, positionFlags)) { BeginScrollableContent("##PositionScroll"); if (MatchesAnySearch({ PrecipitationSetting::kCenterOffsetMin, PrecipitationSetting::kCenterOffsetMax, PrecipitationSetting::kStartRotationRange })) { - ImGui::SeparatorText("Offset"); + ImGui::SeparatorText(T(TKEY("offset"), "Offset")); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kCenterOffsetMin, settings.centerOffsetMin, 0.0f, 200.0f); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kCenterOffsetMax, settings.centerOffsetMax, 0.0f, 200.0f); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kStartRotationRange, settings.startRotationRange, 0.0f, 360.0f); } if (MatchesAnySearch({ PrecipitationSetting::kBoxSize, PrecipitationSetting::kParticleDensity })) { - ImGui::SeparatorText("Volume"); + ImGui::SeparatorText(T(TKEY("volume"), "Volume")); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kBoxSize, settings.boxSize, 0.0f, 1000.0f); changed |= WeatherUtils::DrawSliderFloat(PrecipitationSetting::kParticleDensity, settings.particleDensity, 0.0f, 1000.0f); } @@ -94,33 +99,33 @@ void PrecipitationWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(PrecipitationTab::kTexture, nullptr, textureFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_texture"), "Texture"), nullptr, textureFlags)) { BeginScrollableContent("##TextureScroll"); if (MatchesAnySearch({ PrecipitationSetting::kNumSubtexturesX, PrecipitationSetting::kNumSubtexturesY })) { - ImGui::SeparatorText("Subtextures"); + ImGui::SeparatorText(T(TKEY("subtextures"), "Subtextures")); int numX = static_cast(settings.numSubtexturesX); int numY = static_cast(settings.numSubtexturesY); if (DrawIfMatchesSearch(PrecipitationSetting::kNumSubtexturesX, [&](const char* label) { - return DrawWithHighlight(label, [&]() { - return ImGui::InputInt(label, &numX); - }); - })) { + return DrawWithHighlight(label, [&]() { + return ImGui::InputInt(std::format("{}##{}", T(TKEY("num_subtextures_x"), "Num Subtextures X"), PrecipitationSetting::kNumSubtexturesX).c_str(), &numX); + }); + })) { settings.numSubtexturesX = std::max(1, numX); changed = true; } if (DrawIfMatchesSearch(PrecipitationSetting::kNumSubtexturesY, [&](const char* label) { - return DrawWithHighlight(label, [&]() { - return ImGui::InputInt(label, &numY); - }); - })) { + return DrawWithHighlight(label, [&]() { + return ImGui::InputInt(std::format("{}##{}", T(TKEY("num_subtextures_y"), "Num Subtextures Y"), PrecipitationSetting::kNumSubtexturesY).c_str(), &numY); + }); + })) { settings.numSubtexturesY = std::max(1, numY); changed = true; } } DrawSearchSectionIfMatches(PrecipitationSetting::kParticleTexture, [&](const char* label) { - ImGui::SeparatorText("Texture Path"); + ImGui::SeparatorText(T(TKEY("texture_path"), "Texture Path")); const bool inputChanged = DrawWithHighlight(label, [&]() { - return ImGui::InputText(label, textureBuffer, sizeof(textureBuffer)); + return ImGui::InputText(std::format("{}##{}", T(TKEY("particle_texture_label"), "Particle Texture"), PrecipitationSetting::kParticleTexture).c_str(), textureBuffer, sizeof(textureBuffer)); }); std::string_view buf(textureBuffer); if (buf != lastCheckedBuffer) { @@ -133,9 +138,9 @@ void PrecipitationWidget::DrawWidget() } if (settings.particleTexture != buf && !buf.empty()) { if (!WeatherUtils::TexturePath::HasDdsExtension(buf)) - ImGui::TextColored(globals::menu->GetTheme().StatusPalette.Error, "Path must end with '.dds'"); + ImGui::TextColored(globals::menu->GetTheme().StatusPalette.Error, "%s", T(TKEY("path_must_end_dds"), "Path must end with '.dds'")); else if (!lastCheckedExists) - ImGui::TextColored(globals::menu->GetTheme().StatusPalette.Error, "Texture file not found under Data/textures/."); + ImGui::TextColored(globals::menu->GetTheme().StatusPalette.Error, "%s", T(TKEY("texture_file_not_found"), "Texture file not found under Data/textures/.")); } }); @@ -152,6 +157,8 @@ void PrecipitationWidget::DrawWidget() ImGui::End(); } +#undef I18N_KEY_PREFIX + void PrecipitationWidget::LoadSettings() { if (!precipitation) @@ -345,7 +352,7 @@ std::vector PrecipitationWidget::CollectSearchableSettings std::vector results; for (const auto& [tab, names] : entries) { for (const auto& name : names) { - results.push_back({ name, tab, name }); + results.push_back({ WeatherUtils::TranslateControlLabel(name), tab, name }); } } return results; diff --git a/src/CSEditor/Weather/ReferenceEffectWidget.cpp b/src/CSEditor/Weather/ReferenceEffectWidget.cpp index bffbd38038..f1b38e797d 100644 --- a/src/CSEditor/Weather/ReferenceEffectWidget.cpp +++ b/src/CSEditor/Weather/ReferenceEffectWidget.cpp @@ -1,7 +1,12 @@ #include "ReferenceEffectWidget.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "../WeatherUtils.h" +#include + +#define I18N_KEY_PREFIX "cs_editor." + namespace { namespace ReferenceEffectSetting @@ -26,15 +31,19 @@ void ReferenceEffectWidget::DrawWidget() auto editorWindow = EditorWindow::GetSingleton(); auto drawFormPicker = [&](const char* label, auto& currentForm, const auto& widgets) { + const char* displayLabel = label == ReferenceEffectSetting::kArtObject ? + T(TKEY("art_object"), "Art Object") : + T(TKEY("effect_shader"), "Effect Shader"); + const auto controlLabel = std::format("{}##{}", displayLabel, label); return DrawWithHighlight(label, [&]() { - return WeatherUtils::DrawFormPickerCached(label, currentForm, widgets, false, true); + return WeatherUtils::DrawFormPickerCached(controlLabel.c_str(), currentForm, widgets, false, true); }); }; if (DrawIfMatchesSearch(ReferenceEffectSetting::kArtObject, [&](const char* label) { ImGui::SeparatorText(label); if (editorWindow->artObjectWidgets.empty()) { - ImGui::TextDisabled("No Art Objects available"); + ImGui::TextDisabled("%s", T(TKEY("no_art_objects_available"), "No Art Objects available")); return false; } return drawFormPicker(label, settings.artObject, editorWindow->artObjectWidgets); @@ -43,14 +52,14 @@ void ReferenceEffectWidget::DrawWidget() if (DrawIfMatchesSearch(ReferenceEffectSetting::kEffectShader, [&](const char* label) { ImGui::SeparatorText(label); if (editorWindow->effectShaderWidgets.empty()) { - ImGui::TextDisabled("No Effect Shaders available"); + ImGui::TextDisabled("%s", T(TKEY("no_effect_shaders_available"), "No Effect Shaders available")); return false; } return drawFormPicker(label, settings.effectShader, editorWindow->effectShaderWidgets); })) changed = true; if (MatchesAnySearch({ ReferenceEffectSetting::kFaceTarget, ReferenceEffectSetting::kAttachToCamera, ReferenceEffectSetting::kInheritRotation })) { - ImGui::SeparatorText("Flags"); + ImGui::SeparatorText(T(TKEY("flags"), "Flags")); if (WeatherUtils::DrawCheckbox(ReferenceEffectSetting::kFaceTarget, settings.faceTarget)) changed = true; if (WeatherUtils::DrawCheckbox(ReferenceEffectSetting::kAttachToCamera, settings.attachToCamera)) @@ -69,6 +78,8 @@ void ReferenceEffectWidget::DrawWidget() ImGui::End(); } +#undef I18N_KEY_PREFIX + void ReferenceEffectWidget::LoadSettings() { if (!referenceEffect) @@ -167,10 +178,10 @@ bool ReferenceEffectWidget::HasUnsavedChanges() const std::vector ReferenceEffectWidget::CollectSearchableSettings() const { return { - { ReferenceEffectSetting::kArtObject, "", ReferenceEffectSetting::kArtObject }, - { ReferenceEffectSetting::kEffectShader, "", ReferenceEffectSetting::kEffectShader }, - { ReferenceEffectSetting::kFaceTarget, "", ReferenceEffectSetting::kFaceTarget }, - { ReferenceEffectSetting::kAttachToCamera, "", ReferenceEffectSetting::kAttachToCamera }, - { ReferenceEffectSetting::kInheritRotation, "", ReferenceEffectSetting::kInheritRotation }, + { WeatherUtils::TranslateControlLabel(ReferenceEffectSetting::kArtObject), "", ReferenceEffectSetting::kArtObject }, + { WeatherUtils::TranslateControlLabel(ReferenceEffectSetting::kEffectShader), "", ReferenceEffectSetting::kEffectShader }, + { WeatherUtils::TranslateControlLabel(ReferenceEffectSetting::kFaceTarget), "", ReferenceEffectSetting::kFaceTarget }, + { WeatherUtils::TranslateControlLabel(ReferenceEffectSetting::kAttachToCamera), "", ReferenceEffectSetting::kAttachToCamera }, + { WeatherUtils::TranslateControlLabel(ReferenceEffectSetting::kInheritRotation), "", ReferenceEffectSetting::kInheritRotation }, }; } diff --git a/src/CSEditor/Weather/SimpleFormWidget.h b/src/CSEditor/Weather/SimpleFormWidget.h index 6bad8154a8..98efd67d4c 100644 --- a/src/CSEditor/Weather/SimpleFormWidget.h +++ b/src/CSEditor/Weather/SimpleFormWidget.h @@ -1,5 +1,6 @@ #pragma once +#include "../../I18n/I18n.h" #include "../Widget.h" // Simple widget for displaying form information without editing @@ -22,11 +23,11 @@ class SimpleFormWidget : public Widget void DrawWidget() override { - ImGui::Text("EditorID: %s", editorID.c_str()); - ImGui::Text("FormID: %08X", formID); - ImGui::Text("File: %s", filename.c_str()); + ImGui::Text(T("cs_editor.editor_id_label", "EditorID: %s"), editorID.c_str()); + ImGui::Text(T("cs_editor.form_id_label", "FormID: %08X"), formID); + ImGui::Text(T("cs_editor.file_label", "File: %s"), filename.c_str()); ImGui::Separator(); - ImGui::TextWrapped("This form is referenced by weather records. To change which form is used, edit the Records tab in the Weather widget."); + ImGui::TextWrapped("%s", T("cs_editor.form_reference_note", "This form is referenced by weather records. To change which form is used, edit the Records tab in the Weather widget.")); } void LoadSettings() override {} diff --git a/src/CSEditor/Weather/VolumetricLightingWidget.cpp b/src/CSEditor/Weather/VolumetricLightingWidget.cpp index f032a7ef8d..8f24921c3e 100644 --- a/src/CSEditor/Weather/VolumetricLightingWidget.cpp +++ b/src/CSEditor/Weather/VolumetricLightingWidget.cpp @@ -1,7 +1,10 @@ #include "VolumetricLightingWidget.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "../WeatherUtils.h" +#define I18N_KEY_PREFIX "cs_editor." + namespace { namespace VolumetricLightingTab @@ -44,15 +47,15 @@ void VolumetricLightingWidget::DrawWidget() }); }; - if (ImGui::BeginTabItem(VolumetricLightingTab::kBasic, nullptr, basicFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_basic"), "Basic"), nullptr, basicFlags)) { BeginScrollableContent("##BasicScroll"); - drawSection(VolumetricLightingSetting::kIntensity, "Intensity", [&]() { + drawSection(VolumetricLightingSetting::kIntensity, T(TKEY("intensity"), "Intensity"), [&]() { changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kIntensity, settings.intensity, 0.0f, 50.0f); }); - drawSection(VolumetricLightingSetting::kContribution, "Custom Color", [&]() { + drawSection(VolumetricLightingSetting::kContribution, T(TKEY("custom_color"), "Custom Color"), [&]() { changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kContribution, settings.customColorContribution, 0.0f, 1.0f); }); - drawSection(VolumetricLightingSetting::kColor, "RGB Color", [&]() { + drawSection(VolumetricLightingSetting::kColor, T(TKEY("rgb_color"), "RGB Color"), [&]() { float3 rgbColor{ settings.red, settings.green, settings.blue }; if (WeatherUtils::DrawColorEdit(VolumetricLightingSetting::kColor, rgbColor)) { settings.red = rgbColor.x; @@ -65,10 +68,10 @@ void VolumetricLightingWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(VolumetricLightingTab::kDensity, nullptr, densityFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_density"), "Density"), nullptr, densityFlags)) { BeginScrollableContent("##DensityScroll"); if (MatchesAnySearch({ VolumetricLightingSetting::kContribution, VolumetricLightingSetting::kSize, VolumetricLightingSetting::kWindSpeed, VolumetricLightingSetting::kFallingSpeed })) { - ImGui::SeparatorText("Density Settings"); + ImGui::SeparatorText(T(TKEY("density_settings"), "Density Settings")); changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kContribution, settings.densityContribution, 0.0f, 1.0f); changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kSize, settings.densitySize, 0.1f, 10000.0f); changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kWindSpeed, settings.densityWindSpeed, 0.0f, 100.0f); @@ -78,14 +81,14 @@ void VolumetricLightingWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(VolumetricLightingTab::kAdvanced, nullptr, advancedFlags)) { + if (ImGui::BeginTabItem(T(TKEY("tab_advanced"), "Advanced"), nullptr, advancedFlags)) { BeginScrollableContent("##AdvancedScroll"); if (MatchesAnySearch({ VolumetricLightingSetting::kContribution, VolumetricLightingSetting::kScattering })) { - ImGui::SeparatorText("Phase Function"); + ImGui::SeparatorText(T(TKEY("phase_function"), "Phase Function")); changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kContribution, settings.phaseFunctionContribution, 0.0f, 1.0f); changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kScattering, settings.phaseFunctionScattering, -1.0f, 1.0f); } - drawSection(VolumetricLightingSetting::kRangeFactor, "Sampling", [&]() { + drawSection(VolumetricLightingSetting::kRangeFactor, T(TKEY("sampling"), "Sampling"), [&]() { changed |= WeatherUtils::DrawSliderFloat(VolumetricLightingSetting::kRangeFactor, settings.samplingRangeFactor, 0.0f, 160.0f); }); EndScrollableContent(); @@ -215,15 +218,17 @@ std::vector VolumetricLightingWidget::CollectSearchableSet // Many tabs share the same inner label ("Contribution"); display names are // disambiguated for the dropdown while the inner id matches the ImGui label. return { - { VolumetricLightingSetting::kIntensity, VolumetricLightingTab::kBasic, VolumetricLightingSetting::kIntensity }, - { "Custom Color Contribution", VolumetricLightingTab::kBasic, VolumetricLightingSetting::kContribution }, - { VolumetricLightingSetting::kColor, VolumetricLightingTab::kBasic, VolumetricLightingSetting::kColor }, - { "Density Contribution", VolumetricLightingTab::kDensity, VolumetricLightingSetting::kContribution }, - { "Density Size", VolumetricLightingTab::kDensity, VolumetricLightingSetting::kSize }, - { VolumetricLightingSetting::kWindSpeed, VolumetricLightingTab::kDensity, VolumetricLightingSetting::kWindSpeed }, - { VolumetricLightingSetting::kFallingSpeed, VolumetricLightingTab::kDensity, VolumetricLightingSetting::kFallingSpeed }, - { "Phase Function Contribution", VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kContribution }, - { "Phase Function Scattering", VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kScattering }, - { "Sampling Range Factor", VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kRangeFactor }, + { WeatherUtils::TranslateControlLabel(VolumetricLightingSetting::kIntensity), VolumetricLightingTab::kBasic, VolumetricLightingSetting::kIntensity }, + { T(TKEY("custom_color_contribution"), "Custom Color Contribution"), VolumetricLightingTab::kBasic, VolumetricLightingSetting::kContribution }, + { WeatherUtils::TranslateControlLabel(VolumetricLightingSetting::kColor), VolumetricLightingTab::kBasic, VolumetricLightingSetting::kColor }, + { T(TKEY("density_contribution"), "Density Contribution"), VolumetricLightingTab::kDensity, VolumetricLightingSetting::kContribution }, + { T(TKEY("density_size"), "Density Size"), VolumetricLightingTab::kDensity, VolumetricLightingSetting::kSize }, + { WeatherUtils::TranslateControlLabel(VolumetricLightingSetting::kWindSpeed), VolumetricLightingTab::kDensity, VolumetricLightingSetting::kWindSpeed }, + { WeatherUtils::TranslateControlLabel(VolumetricLightingSetting::kFallingSpeed), VolumetricLightingTab::kDensity, VolumetricLightingSetting::kFallingSpeed }, + { T(TKEY("phase_function_contribution"), "Phase Function Contribution"), VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kContribution }, + { T(TKEY("phase_function_scattering"), "Phase Function Scattering"), VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kScattering }, + { T(TKEY("sampling_range_factor"), "Sampling Range Factor"), VolumetricLightingTab::kAdvanced, VolumetricLightingSetting::kRangeFactor }, }; } + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/Weather/WeatherWidget.cpp b/src/CSEditor/Weather/WeatherWidget.cpp index 06995deb2f..19080c838e 100644 --- a/src/CSEditor/Weather/WeatherWidget.cpp +++ b/src/CSEditor/Weather/WeatherWidget.cpp @@ -5,6 +5,7 @@ #include "imgui_internal.h" +#include "../../I18n/I18n.h" #include "../EditorWindow.h" #include "FeatureIssues.h" #include "State.h" @@ -12,6 +13,8 @@ #include "WeatherManager.h" #include "WeatherVariableRegistry.h" +#define I18N_KEY_PREFIX "cs_editor." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Atmosphere, colorTimes) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DirectionalColor, max, min) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DALC, specular, fresnelPower, directional) @@ -69,6 +72,39 @@ namespace constexpr int kVolumetricLightingIdOffset = 100; } + const char* TranslateWeatherPropertyLabel(std::string_view label) + { + if (label == "Sun Damage") + return T(TKEY("sun_damage"), "Sun Damage"); + if (label == "Wind Speed") + return T(TKEY("wind_speed"), "Wind Speed"); + if (label == "Wind Direction") + return T(TKEY("wind_direction_label"), "Wind Direction"); + if (label == "Wind Direction Range") + return T(TKEY("wind_direction_range_label"), "Wind Direction Range"); + if (label == "Precipitation Begin Fade In") + return T(TKEY("precipitation_begin_fade_in_label"), "Precipitation Begin Fade In"); + if (label == "Precipitation End Fade Out") + return T(TKEY("precipitation_end_fade_out_label"), "Precipitation End Fade Out"); + if (label == "Thunder Lightning Begin Fade In") + return T(TKEY("thunder_lightning_begin_fade_in"), "Thunder Lightning Begin Fade In"); + if (label == "Thunder Lightning End Fade Out") + return T(TKEY("thunder_lightning_end_fade_out"), "Thunder Lightning End Fade Out"); + if (label == "Thunder Lightning Frequency") + return T(TKEY("thunder_lightning_frequency"), "Thunder Lightning Frequency"); + if (label == "Lightning Color") + return T(TKEY("lightning_color_label"), "Lightning Color"); + if (label == "Visual Effect Begin") + return T(TKEY("visual_effect_begin"), "Visual Effect Begin"); + if (label == "Visual Effect End") + return T(TKEY("visual_effect_end"), "Visual Effect End"); + if (label == "Trans Delta") + return T(TKEY("trans_delta"), "Trans Delta"); + + // Fallback: return the original label via T() which caches a stable null-terminated copy + return T(std::string(label).c_str(), std::string(label).c_str()); + } + namespace WeatherInherit { constexpr const char* kDalcSpecular = "DALC_Specular"; @@ -127,9 +163,9 @@ void WeatherWidget::DrawWidget() } if (editorWindow->settings.enableInheritFromParent) { - if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { + if (ImGui::BeginCombo(T(TKEY("parent"), "Parent"), settings.parent.c_str())) { // Option for "None" - if (ImGui::Selectable("None", parent == nullptr)) { + if (ImGui::Selectable(T(TKEY("none"), "None"), parent == nullptr)) { parent = nullptr; settings.parent = "None"; } @@ -158,22 +194,22 @@ void WeatherWidget::DrawWidget() ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); - ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); - Util::Text::Warning("Note: This is NOT the same as cell lighting template inheritance."); + ImGui::TextUnformatted(T(TKEY("parent_cs_editor_feature"), "Editor-only feature: Set a parent weather to copy settings from.")); + ImGui::TextUnformatted(T(TKEY("use_inherit_checkboxes"), "Use 'Inherit From Parent' checkboxes to copy specific values.")); + Util::Text::Warning("%s", T(TKEY("not_same_as_cell_lighting"), "Note: This is NOT the same as cell lighting template inheritance.")); ImGui::EndTooltip(); } if (parent) { ImGui::SameLine(); - if (Util::ButtonWithFlash("Inherit All")) { + if (Util::ButtonWithFlash(T(TKEY("inherit_all"), "Inherit All"))) { InheritAllFromParent(); } - Util::AddTooltip("Copy all parameter values from parent weather"); + Util::AddTooltip(T(TKEY("copy_all_from_parent"), "Copy all parameter values from parent weather")); if (!parent->IsOpen()) { ImGui::SameLine(); - if (Util::ButtonWithFlash("Open")) + if (Util::ButtonWithFlash(T(TKEY("open"), "Open"))) parent->SetOpen(true); } } @@ -190,57 +226,57 @@ void WeatherWidget::DrawWidget() const ImGuiTabItemFlags featuresFlags = GetTabFlagsForOverride("Features"); const ImGuiTabItemFlags recordsFlags = GetTabFlagsForOverride(WeatherTab::kRecords); - if (ImGui::BeginTabItem(WeatherTab::kBasic, nullptr, basicFlags)) { + if (ImGui::BeginTabItem(T(TKEY("basic"), WeatherTab::kBasic), nullptr, basicFlags)) { BeginScrollableContent("##BasicScroll"); - DrawProperties("Sun", { { "Sun Damage", UINT8_SLIDER } }); - DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", UINT8_SLIDER }, { "Wind Direction Range", UINT8_SLIDER } }); - DrawProperties("Precipitation", { { "Precipitation Begin Fade In", UINT8_SLIDER }, { "Precipitation End Fade Out", UINT8_SLIDER } }); - DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", UINT8_SLIDER }, { "Thunder Lightning End Fade Out", UINT8_SLIDER }, - { "Thunder Lightning Frequency", UINT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); - DrawProperties("Visual Effects", { { "Visual Effect Begin", UINT8_SLIDER }, { "Visual Effect End", UINT8_SLIDER } }); - DrawProperties("Weather Transition", { { "Trans Delta", UINT8_SLIDER } }); + DrawProperties(T(TKEY("category_sun"), "Sun"), { { "Sun Damage", UINT8_SLIDER } }); + DrawProperties(T(TKEY("category_wind"), "Wind"), { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", UINT8_SLIDER }, { "Wind Direction Range", UINT8_SLIDER } }); + DrawProperties(T(TKEY("category_precipitation"), "Precipitation"), { { "Precipitation Begin Fade In", UINT8_SLIDER }, { "Precipitation End Fade Out", UINT8_SLIDER } }); + DrawProperties(T(TKEY("category_lightning"), "Lightning"), { { "Thunder Lightning Begin Fade In", UINT8_SLIDER }, { "Thunder Lightning End Fade Out", UINT8_SLIDER }, + { "Thunder Lightning Frequency", UINT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); + DrawProperties(T(TKEY("category_visual_effects"), "Visual Effects"), { { "Visual Effect Begin", UINT8_SLIDER }, { "Visual Effect End", UINT8_SLIDER } }); + DrawProperties(T(TKEY("category_weather_transition"), "Weather Transition"), { { "Trans Delta", UINT8_SLIDER } }); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(WeatherTab::kDalc, nullptr, dalcFlags)) { + if (ImGui::BeginTabItem(T(TKEY("lighting_dalc"), WeatherTab::kDalc), nullptr, dalcFlags)) { BeginScrollableContent("##DALCScroll"); DrawDALCSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(WeatherTab::kAtmosphere, nullptr, atmosphereFlags)) { + if (ImGui::BeginTabItem(T(TKEY("atmosphere_colors"), WeatherTab::kAtmosphere), nullptr, atmosphereFlags)) { BeginScrollableContent("##AtmosphereScroll"); DrawWeatherColorSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(WeatherTab::kClouds, nullptr, cloudsFlags)) { + if (ImGui::BeginTabItem(T(TKEY("clouds"), WeatherTab::kClouds), nullptr, cloudsFlags)) { BeginScrollableContent("##CloudsScroll"); DrawCloudSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(WeatherTab::kFog, nullptr, fogFlags)) { + if (ImGui::BeginTabItem(T(TKEY("fog"), WeatherTab::kFog), nullptr, fogFlags)) { BeginScrollableContent("##FogScroll"); DrawFogSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { + if (ImGui::BeginTabItem(T(TKEY("features"), "Features"), nullptr, featuresFlags)) { BeginScrollableContent("##FeaturesScroll"); DrawFeatureSettings(); EndScrollableContent(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem(WeatherTab::kRecords, nullptr, recordsFlags)) { + if (ImGui::BeginTabItem(T(TKEY("records"), WeatherTab::kRecords), nullptr, recordsFlags)) { BeginScrollableContent("##RecordsScroll"); ImGui::Spacing(); - ImGui::TextWrapped("Form record references used by this weather."); + ImGui::TextWrapped("%s", T(TKEY("form_record_references"), "Form record references used by this weather.")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -287,7 +323,7 @@ void WeatherWidget::DrawWidget() recordRef = parentRef; pendingReinit = true; } - Util::AddTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + Util::AddTooltip(inheritFlag ? T(TKEY("inheriting_from_parent"), "Inheriting from parent") : T(TKEY("inherit_from_parent"), "Inherit from parent")); ImGui::SameLine(); return inheritFlag; }; @@ -311,13 +347,18 @@ void WeatherWidget::DrawWidget() const bool isInherited = drawInheritCheckbox(inheritKey, recordRefs[i], parentRefs[i]); ImGui::Text("%s:", label.c_str()); ImGui::SameLine(todLabelOffset); - if (isInherited) PushInheritedStyle(); + if (isInherited) + PushInheritedStyle(); if (WeatherUtils::DrawFormPickerCached(pickerId, recordRefs[i], widgets, false, true, pickerWidth)) { pendingReinit = true; - if (isInherited) settings.inheritFlags[inheritKey] = false; + if (isInherited) + settings.inheritFlags[inheritKey] = false; } - if (isInherited) { Util::AddTooltip("Inherited from parent weather"); PopInheritedStyle(); } - drawOpenButton(recordRefs[i], widgets, std::format("Open##{}", i), openTooltip); + if (isInherited) { + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); + PopInheritedStyle(); + } + drawOpenButton(recordRefs[i], widgets, std::format("{}##{}", T(TKEY("open"), "Open"), i), openTooltip); if (recordHighlighted) PopHighlightIfNeeded(rowId, recordHighlighted); @@ -325,7 +366,7 @@ void WeatherWidget::DrawWidget() } ImGui::Spacing(); }; - auto drawSingleRecordSection = [&](const char* sectionLabel, const char* recordId, const char* inheritKey, const char* valueLabel, const char* pickerId, auto& recordRef, auto& parentRef, auto& widgets, const char* buttonId, const char* openTooltip) { + auto drawSingleRecordSection = [&](const char* sectionLabel, const char* recordId, const char* inheritKey, const char* valueLabel, const char* pickerId, auto& recordRef, auto& parentRef, auto& widgets, const std::string& buttonId, const char* openTooltip) { if (!MatchesSearch(recordId)) return; if (ShouldOpenSearchSection()) @@ -337,12 +378,17 @@ void WeatherWidget::DrawWidget() const bool isInherited = drawInheritCheckbox(inheritKey, recordRef, parentRef); ImGui::Text("%s:", valueLabel); ImGui::SameLine(formLabelOffset); - if (isInherited) PushInheritedStyle(); + if (isInherited) + PushInheritedStyle(); if (WeatherUtils::DrawFormPickerCached(pickerId, recordRef, widgets, false, true, pickerWidth)) { pendingReinit = true; - if (isInherited) settings.inheritFlags[inheritKey] = false; + if (isInherited) + settings.inheritFlags[inheritKey] = false; + } + if (isInherited) { + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); + PopInheritedStyle(); } - if (isInherited) { Util::AddTooltip("Inherited from parent weather"); PopInheritedStyle(); } drawOpenButton(recordRef, widgets, buttonId, openTooltip); if (recordHighlighted) @@ -354,10 +400,10 @@ void WeatherWidget::DrawWidget() auto* parentVolumetricLightingRefs = parentWidget ? parentWidget->settings.volumetricLightingRefs : settings.volumetricLightingRefs; auto* parentPrecipitationData = parentWidget ? parentWidget->settings.precipitationData : settings.precipitationData; auto* parentReferenceEffect = parentWidget ? parentWidget->settings.referenceEffect : settings.referenceEffect; - drawTimeRecordSection(WeatherRecord::kImageSpace, WeatherRecord::kImageSpaceIdOffset, WeatherRecord::kImageSpace, settings.imageSpaceRefs, parentImageSpaceRefs, editorWindow->imageSpaceWidgets, "##ImageSpace", "Open this ImageSpace for editing"); - drawTimeRecordSection(WeatherRecord::kVolumetricLighting, WeatherRecord::kVolumetricLightingIdOffset, "VolumetricLighting", settings.volumetricLightingRefs, parentVolumetricLightingRefs, editorWindow->volumetricLightingWidgets, "##VolumetricLighting", "Open this Volumetric Lighting for editing"); - drawSingleRecordSection(WeatherRecord::kPrecipitation, WeatherRecord::kPrecipitation, WeatherRecord::kPrecipitation, "Particle Shader", "##Precipitation", settings.precipitationData, parentPrecipitationData, editorWindow->precipitationWidgets, "Open##Precip", "Open this Precipitation for editing"); - drawSingleRecordSection(WeatherRecord::kVisualEffect, WeatherRecord::kVisualEffect, "ReferenceEffect", WeatherRecord::kVisualEffect, "##ReferenceEffect", settings.referenceEffect, parentReferenceEffect, editorWindow->referenceEffectWidgets, "Open##RefEffect", "Open this Visual Effect for editing"); + drawTimeRecordSection(T(TKEY("record_imagespace"), "ImageSpace"), WeatherRecord::kImageSpaceIdOffset, WeatherRecord::kImageSpace, settings.imageSpaceRefs, parentImageSpaceRefs, editorWindow->imageSpaceWidgets, "##ImageSpace", T(TKEY("open_imagespace_edit"), "Open this ImageSpace for editing")); + drawTimeRecordSection(T(TKEY("record_volumetric_lighting"), "Volumetric Lighting"), WeatherRecord::kVolumetricLightingIdOffset, "VolumetricLighting", settings.volumetricLightingRefs, parentVolumetricLightingRefs, editorWindow->volumetricLightingWidgets, "##VolumetricLighting", T(TKEY("open_volumetric_edit"), "Open this Volumetric Lighting for editing")); + drawSingleRecordSection(T(TKEY("record_precipitation"), "Precipitation"), WeatherRecord::kPrecipitation, WeatherRecord::kPrecipitation, T(TKEY("particle_shader"), "Particle Shader"), "##Precipitation", settings.precipitationData, parentPrecipitationData, editorWindow->precipitationWidgets, std::format("{}##Precip", T(TKEY("open"), "Open")), T(TKEY("open_precipitation_edit"), "Open this Precipitation for editing")); + drawSingleRecordSection(T(TKEY("record_visual_effect"), "Visual Effect"), WeatherRecord::kVisualEffect, "ReferenceEffect", T(TKEY("record_visual_effect"), "Visual Effect"), "##ReferenceEffect", settings.referenceEffect, parentReferenceEffect, editorWindow->referenceEffectWidgets, std::format("{}##RefEffect", T(TKEY("open"), "Open")), T(TKEY("open_visual_effect_edit"), "Open this Visual Effect for editing")); if (pendingReinit) { ApplyChanges(); @@ -821,12 +867,12 @@ void WeatherWidget::DrawDALCSettings() } // Draw with per-parameter inheritance - auto drawDalcColor = [&](const char* settingId, const char* label, float3 (&values)[4], bool* inheritFlag = nullptr, float3* parentValues = nullptr) { + auto drawDalcColor = [&](const char* settingId, const char* label, float3(&values)[4], bool* inheritFlag = nullptr, float3* parentValues = nullptr) { return DrawIfMatchesSearch(settingId, [&](const char*) { return DrawWithHighlight(settingId, [&]() { return inheritFlag ? - TOD::DrawTODColorRow(label, values, *inheritFlag, parentValues) : - TOD::DrawTODColorRow(label, values); + TOD::DrawTODColorRow(label, values, *inheritFlag, parentValues) : + TOD::DrawTODColorRow(label, values); }); }); }; @@ -834,32 +880,32 @@ void WeatherWidget::DrawDALCSettings() return DrawIfMatchesSearch(settingId, [&](const char*) { return DrawWithHighlight(settingId, [&]() { return inheritFlag ? - TOD::DrawTODFloatRow(label, values, *inheritFlag, parentValues, 0.0f, 10.0f) : - TOD::DrawTODFloatRow(label, values, 0.0f, 10.0f); + TOD::DrawTODFloatRow(label, values, *inheritFlag, parentValues, 0.0f, 10.0f) : + TOD::DrawTODFloatRow(label, values, 0.0f, 10.0f); }); }); }; if (hasParent && parentWidget) { - if (drawDalcColor(WeatherSetting::kSpecular, WeatherSetting::kSpecular, specularColors, &settings.inheritFlags[WeatherInherit::kDalcSpecular], parentSpecular)) { + if (drawDalcColor(WeatherSetting::kSpecular, T(TKEY("dalc_specular"), "Specular"), specularColors, &settings.inheritFlags[WeatherInherit::kDalcSpecular], parentSpecular)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].specular = specularColors[i]; changed = true; } - if (drawDalcFloat(WeatherSetting::kFresnelPower, WeatherSetting::kFresnelPower, fresnelPowers, &settings.inheritFlags[WeatherInherit::kDalcFresnel], parentFresnel)) { + if (drawDalcFloat(WeatherSetting::kFresnelPower, T(TKEY("dalc_fresnel_power"), "Fresnel Power"), fresnelPowers, &settings.inheritFlags[WeatherInherit::kDalcFresnel], parentFresnel)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].fresnelPower = fresnelPowers[i]; changed = true; } } else { - if (drawDalcColor(WeatherSetting::kSpecular, WeatherSetting::kSpecular, specularColors)) { + if (drawDalcColor(WeatherSetting::kSpecular, T(TKEY("dalc_specular"), "Specular"), specularColors)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].specular = specularColors[i]; changed = true; } - if (drawDalcFloat(WeatherSetting::kFresnelPower, WeatherSetting::kFresnelPower, fresnelPowers)) { + if (drawDalcFloat(WeatherSetting::kFresnelPower, T(TKEY("dalc_fresnel_power"), "Fresnel Power"), fresnelPowers)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].fresnelPower = fresnelPowers[i]; changed = true; @@ -870,73 +916,73 @@ void WeatherWidget::DrawDALCSettings() // Directional colors with per-parameter inheritance if (hasParent && parentWidget) { - if (drawDalcColor(WeatherSetting::kDirectionalXMax, WeatherDisplay::kDirectionalXMax, directionalXMax, &settings.inheritFlags[WeatherInherit::kDalcDirXMax], parentDirXMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalXMax, T(TKEY("dalc_directional_x_max"), "Directional +X"), directionalXMax, &settings.inheritFlags[WeatherInherit::kDalcDirXMax], parentDirXMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].max = directionalXMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalXMin, WeatherDisplay::kDirectionalXMin, directionalXMin, &settings.inheritFlags[WeatherInherit::kDalcDirXMin], parentDirXMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalXMin, T(TKEY("dalc_directional_x_min"), "Directional -X"), directionalXMin, &settings.inheritFlags[WeatherInherit::kDalcDirXMin], parentDirXMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].min = directionalXMin[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalYMax, WeatherDisplay::kDirectionalYMax, directionalYMax, &settings.inheritFlags[WeatherInherit::kDalcDirYMax], parentDirYMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalYMax, T(TKEY("dalc_directional_y_max"), "Directional +Y"), directionalYMax, &settings.inheritFlags[WeatherInherit::kDalcDirYMax], parentDirYMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].max = directionalYMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalYMin, WeatherDisplay::kDirectionalYMin, directionalYMin, &settings.inheritFlags[WeatherInherit::kDalcDirYMin], parentDirYMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalYMin, T(TKEY("dalc_directional_y_min"), "Directional -Y"), directionalYMin, &settings.inheritFlags[WeatherInherit::kDalcDirYMin], parentDirYMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].min = directionalYMin[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalZMax, WeatherDisplay::kDirectionalZMax, directionalZMax, &settings.inheritFlags[WeatherInherit::kDalcDirZMax], parentDirZMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalZMax, T(TKEY("dalc_directional_z_max"), "Directional +Z"), directionalZMax, &settings.inheritFlags[WeatherInherit::kDalcDirZMax], parentDirZMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].max = directionalZMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalZMin, WeatherDisplay::kDirectionalZMin, directionalZMin, &settings.inheritFlags[WeatherInherit::kDalcDirZMin], parentDirZMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalZMin, T(TKEY("dalc_directional_z_min"), "Directional -Z"), directionalZMin, &settings.inheritFlags[WeatherInherit::kDalcDirZMin], parentDirZMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].min = directionalZMin[i]; changed = true; } } else { - if (drawDalcColor(WeatherSetting::kDirectionalXMax, WeatherDisplay::kDirectionalXMax, directionalXMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalXMax, T(TKEY("dalc_directional_x_max"), "Directional +X"), directionalXMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].max = directionalXMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalXMin, WeatherDisplay::kDirectionalXMin, directionalXMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalXMin, T(TKEY("dalc_directional_x_min"), "Directional -X"), directionalXMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].min = directionalXMin[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalYMax, WeatherDisplay::kDirectionalYMax, directionalYMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalYMax, T(TKEY("dalc_directional_y_max"), "Directional +Y"), directionalYMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].max = directionalYMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalYMin, WeatherDisplay::kDirectionalYMin, directionalYMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalYMin, T(TKEY("dalc_directional_y_min"), "Directional -Y"), directionalYMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].min = directionalYMin[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalZMax, WeatherDisplay::kDirectionalZMax, directionalZMax)) { + if (drawDalcColor(WeatherSetting::kDirectionalZMax, T(TKEY("dalc_directional_z_max"), "Directional +Z"), directionalZMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].max = directionalZMax[i]; changed = true; } - if (drawDalcColor(WeatherSetting::kDirectionalZMin, WeatherDisplay::kDirectionalZMin, directionalZMin)) { + if (drawDalcColor(WeatherSetting::kDirectionalZMin, T(TKEY("dalc_directional_z_min"), "Directional -Z"), directionalZMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].min = directionalZMin[i]; changed = true; @@ -1025,7 +1071,7 @@ void WeatherWidget::DrawCloudSettings() // OpenOnArrow|OpenOnDoubleClick prevents accidental collapse when clicking // the [Enabled] badge area that overlaps the right side of the header. constexpr ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - constexpr char kEnabledBadge[] = "[Enabled]"; + const char* kEnabledBadge = T(TKEY("enabled_badge"), "[Enabled]"); for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); @@ -1083,7 +1129,7 @@ void WeatherWidget::DrawCloudSettings() // Begin horizontal layout for enable checkbox and sliders on left, texture on right ImGui::BeginGroup(); - if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { + if (ImGui::Checkbox(std::format("{}##{}", T(TKEY("enable"), "Enable"), layer).c_str(), &layerEnabled)) { settings.clouds[i].enabled = layerEnabled; enableChanged = true; changed = true; @@ -1145,21 +1191,21 @@ void WeatherWidget::DrawCloudSettings() std::string alphaKey = std::format("Cloud{}_Alpha", i); drawCloudTODRows([&]() { - if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color, settings.inheritFlags[colorKey], parentColors)) { + if (TOD::DrawTODColorRow(T(TKEY("cloud_color"), "Cloud Color"), settings.clouds[i].color, settings.inheritFlags[colorKey], parentColors)) { changed = true; } - if (TOD::DrawTODFloatRow("Cloud Alpha", settings.clouds[i].cloudAlpha, settings.inheritFlags[alphaKey], parentAlphas, 0.0f, 1.0f)) { + if (TOD::DrawTODFloatRow(T(TKEY("cloud_alpha"), "Cloud Alpha"), settings.clouds[i].cloudAlpha, settings.inheritFlags[alphaKey], parentAlphas, 0.0f, 1.0f)) { changed = true; } }); } else { drawCloudTODRows([&]() { - if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color)) { + if (TOD::DrawTODColorRow(T(TKEY("cloud_color"), "Cloud Color"), settings.clouds[i].color)) { changed = true; } - if (TOD::DrawTODFloatRow("Cloud Alpha", settings.clouds[i].cloudAlpha, 0.0f, 1.0f)) { + if (TOD::DrawTODFloatRow(T(TKEY("cloud_alpha"), "Cloud Alpha"), settings.clouds[i].cloudAlpha, 0.0f, 1.0f)) { changed = true; } }); @@ -1200,19 +1246,19 @@ void WeatherWidget::DrawFogSettings() const float scale = Util::GetUIScale(); if (ImGui::BeginTable("FogTable", 3, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchSame)) { - ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 80.0f * scale); - ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthStretch, 1.0f); - ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthStretch, 1.0f); + ImGui::TableSetupColumn(T(TKEY("parameter"), "Parameter"), ImGuiTableColumnFlags_WidthFixed, 80.0f * scale); + ImGui::TableSetupColumn(T(TKEY("day"), "Day"), ImGuiTableColumnFlags_WidthStretch, 1.0f); + ImGui::TableSetupColumn(T(TKEY("night"), "Night"), ImGuiTableColumnFlags_WidthStretch, 1.0f); // Header row ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(1); ImGui::AlignTextToFramePadding(); - ImGui::Text("Day"); + ImGui::Text("%s", T(TKEY("day"), "Day")); ImGui::TableSetColumnIndex(2); ImGui::AlignTextToFramePadding(); - ImGui::Text("Night"); + ImGui::Text("%s", T(TKEY("night"), "Night")); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -1222,10 +1268,10 @@ void WeatherWidget::DrawFogSettings() ImGui::TableSetColumnIndex(2); ImGui::Separator(); - DrawFogRow(nearMatches, WeatherInherit::kFogNear, "Near", WeatherSetting::kDayNear, WeatherSetting::kNightNear, 0.0f, 1000000.0f, "%.0f", hasParent, parentWidget, changed); - DrawFogRow(farMatches, WeatherInherit::kFogFar, "Far", WeatherSetting::kDayFar, WeatherSetting::kNightFar, 0.0f, 1000000.0f, "%.0f", hasParent, parentWidget, changed); - DrawFogRow(powerMatches, WeatherInherit::kFogPower, "Power", WeatherSetting::kDayPower, WeatherSetting::kNightPower, 0.0f, 10.0f, "%.3f", hasParent, parentWidget, changed); - DrawFogRow(maxMatches, WeatherInherit::kFogMax, "Max", WeatherSetting::kDayMax, WeatherSetting::kNightMax, 0.0f, 1.0f, "%.3f", hasParent, parentWidget, changed); + DrawFogRow(nearMatches, WeatherInherit::kFogNear, T(TKEY("fog_near"), "Near"), WeatherSetting::kDayNear, WeatherSetting::kNightNear, 0.0f, 1000000.0f, "%.0f", hasParent, parentWidget, changed); + DrawFogRow(farMatches, WeatherInherit::kFogFar, T(TKEY("fog_far"), "Far"), WeatherSetting::kDayFar, WeatherSetting::kNightFar, 0.0f, 1000000.0f, "%.0f", hasParent, parentWidget, changed); + DrawFogRow(powerMatches, WeatherInherit::kFogPower, T(TKEY("fog_power_short"), "Power"), WeatherSetting::kDayPower, WeatherSetting::kNightPower, 0.0f, 10.0f, "%.3f", hasParent, parentWidget, changed); + DrawFogRow(maxMatches, WeatherInherit::kFogMax, T(TKEY("fog_max"), "Max"), WeatherSetting::kDayMax, WeatherSetting::kNightMax, 0.0f, 1.0f, "%.3f", hasParent, parentWidget, changed); ImGui::EndTable(); } @@ -1238,12 +1284,16 @@ void WeatherWidget::DrawFogSettings() void WeatherWidget::DrawFogSlider(const char* id, float& prop, float min, float max, const char* fmt, bool& inheritRef, bool isInherited, bool& changed) { ImGui::SetNextItemWidth(-1); - if (isInherited) PushInheritedStyle(); + if (isInherited) + PushInheritedStyle(); if (WeatherUtils::DrawSliderFloat(id, prop, min, max, nullptr, fmt)) { changed = true; inheritRef = false; } - if (isInherited) { Util::AddTooltip("Inherited from parent weather"); PopInheritedStyle(); } + if (isInherited) { + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); + PopInheritedStyle(); + } } void WeatherWidget::DrawFogRow(bool matches, const char* inheritKey, const char* label, @@ -1290,7 +1340,8 @@ void WeatherWidget::DrawFogRow(bool matches, const char* inheritKey, const char* ImGui::TableSetColumnIndex(2); DrawFogSlider(std::format("##Night {}", label).c_str(), settings.fogProperties[nightPropKey], min, max, fmt, inheritRef, isInherited, changed); - if (highlightId) PopHighlightIfNeeded(highlightId, true); + if (highlightId) + PopHighlightIfNeeded(highlightId, true); } void WeatherWidget::DrawProperties(std::string category, std::map properties) @@ -1309,6 +1360,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.weatherColors[key]; - syncFogPair(WeatherInherit::kFogNear, WeatherSetting::kDayNear, WeatherSetting::kNightNear); - syncFogPair(WeatherInherit::kFogFar, WeatherSetting::kDayFar, WeatherSetting::kNightFar); + syncFogPair(WeatherInherit::kFogNear, WeatherSetting::kDayNear, WeatherSetting::kNightNear); + syncFogPair(WeatherInherit::kFogFar, WeatherSetting::kDayFar, WeatherSetting::kNightFar); syncFogPair(WeatherInherit::kFogPower, WeatherSetting::kDayPower, WeatherSetting::kNightPower); - syncFogPair(WeatherInherit::kFogMax, WeatherSetting::kDayMax, WeatherSetting::kNightMax); + syncFogPair(WeatherInherit::kFogMax, WeatherSetting::kDayMax, WeatherSetting::kNightMax); for (int i = 0; i < ColorTypes::kTotal; i++) if (inherited("Atmosphere_" + ColorTypeLabel(i))) settings.atmosphereColors[i] = parentWidget->settings.atmosphereColors[i]; syncDalcTOD(WeatherInherit::kDalcSpecular, [](DALC& d) -> auto& { return d.specular; }); - syncDalcTOD(WeatherInherit::kDalcFresnel, [](DALC& d) -> auto& { return d.fresnelPower; }); - syncDalcTOD(WeatherInherit::kDalcDirXMax, [](DALC& d) -> auto& { return d.directional[0].max; }); - syncDalcTOD(WeatherInherit::kDalcDirXMin, [](DALC& d) -> auto& { return d.directional[0].min; }); - syncDalcTOD(WeatherInherit::kDalcDirYMax, [](DALC& d) -> auto& { return d.directional[1].max; }); - syncDalcTOD(WeatherInherit::kDalcDirYMin, [](DALC& d) -> auto& { return d.directional[1].min; }); - syncDalcTOD(WeatherInherit::kDalcDirZMax, [](DALC& d) -> auto& { return d.directional[2].max; }); - syncDalcTOD(WeatherInherit::kDalcDirZMin, [](DALC& d) -> auto& { return d.directional[2].min; }); + syncDalcTOD(WeatherInherit::kDalcFresnel, [](DALC& d) -> auto& { return d.fresnelPower; }); + syncDalcTOD(WeatherInherit::kDalcDirXMax, [](DALC& d) -> auto& { return d.directional[0].max; }); + syncDalcTOD(WeatherInherit::kDalcDirXMin, [](DALC& d) -> auto& { return d.directional[0].min; }); + syncDalcTOD(WeatherInherit::kDalcDirYMax, [](DALC& d) -> auto& { return d.directional[1].max; }); + syncDalcTOD(WeatherInherit::kDalcDirYMin, [](DALC& d) -> auto& { return d.directional[1].min; }); + syncDalcTOD(WeatherInherit::kDalcDirZMax, [](DALC& d) -> auto& { return d.directional[2].max; }); + syncDalcTOD(WeatherInherit::kDalcDirZMin, [](DALC& d) -> auto& { return d.directional[2].min; }); for (int i = 0; i < TESWeather::kTotalLayers; i++) { if (inherited(std::format("Cloud{}_Color", i))) @@ -1416,11 +1482,11 @@ void WeatherWidget::SyncInheritedValuesFromParent() } for (size_t i = 0; i < ColorTimes::kTotal; i++) { - syncRecord("ImageSpace_" + std::to_string(i), settings.imageSpaceRefs[i], parentWidget->settings.imageSpaceRefs[i]); + syncRecord("ImageSpace_" + std::to_string(i), settings.imageSpaceRefs[i], parentWidget->settings.imageSpaceRefs[i]); syncRecord("VolumetricLighting_" + std::to_string(i), settings.volumetricLightingRefs[i], parentWidget->settings.volumetricLightingRefs[i]); } - syncRecord("Precipitation", settings.precipitationData, parentWidget->settings.precipitationData); - syncRecord("ReferenceEffect", settings.referenceEffect, parentWidget->settings.referenceEffect); + syncRecord("Precipitation", settings.precipitationData, parentWidget->settings.precipitationData); + syncRecord("ReferenceEffect", settings.referenceEffect, parentWidget->settings.referenceEffect); ApplyChanges(); } @@ -1440,7 +1506,10 @@ void WeatherWidget::PropagateToChildren() if (child != this && child->settings.parent == myId) { bool hasAnyInherit = false; for (const auto& [key, val] : child->settings.inheritFlags) - if (val) { hasAnyInherit = true; break; } + if (val) { + hasAnyInherit = true; + break; + } if (hasAnyInherit && !visiting.contains(child)) child->SyncInheritedValuesFromParent(); } @@ -1488,10 +1557,14 @@ void WeatherWidget::InheritAllFromParent() // DALC tab static constexpr const char* kDalcFlags[] = { - WeatherInherit::kDalcSpecular, WeatherInherit::kDalcFresnel, - WeatherInherit::kDalcDirXMax, WeatherInherit::kDalcDirXMin, - WeatherInherit::kDalcDirYMax, WeatherInherit::kDalcDirYMin, - WeatherInherit::kDalcDirZMax, WeatherInherit::kDalcDirZMin, + WeatherInherit::kDalcSpecular, + WeatherInherit::kDalcFresnel, + WeatherInherit::kDalcDirXMax, + WeatherInherit::kDalcDirXMin, + WeatherInherit::kDalcDirYMax, + WeatherInherit::kDalcDirYMin, + WeatherInherit::kDalcDirZMax, + WeatherInherit::kDalcDirZMin, }; for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i] = parentWidget->settings.dalc[i]; @@ -1514,8 +1587,10 @@ void WeatherWidget::InheritAllFromParent() // Fog tab settings.fogProperties = parentWidget->settings.fogProperties; static constexpr const char* kFogFlags[] = { - WeatherInherit::kFogNear, WeatherInherit::kFogFar, - WeatherInherit::kFogPower, WeatherInherit::kFogMax, + WeatherInherit::kFogNear, + WeatherInherit::kFogFar, + WeatherInherit::kFogPower, + WeatherInherit::kFogMax, }; for (const auto* key : kFogFlags) settings.inheritFlags[key] = true; @@ -1724,9 +1799,10 @@ bool WeatherWidget::HasUnsavedChanges() const void WeatherWidget::DrawFeatureSettings() { - ImGui::TextWrapped( - "Configure feature-specific settings that will be applied when this weather is active. " - "These override the feature's global settings for this weather only."); + ImGui::TextWrapped("%s", + T(TKEY("feature_specific_settings"), + "Configure feature-specific settings that will be applied when this weather is active. " + "These override the feature's global settings for this weather only.")); ImGui::Spacing(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); @@ -1744,7 +1820,7 @@ void WeatherWidget::DrawFeatureSettings() continue; } - std::string displayName = feature->GetName(); + std::string displayName = feature->GetDisplayName(); auto featureIt = settings.featureSettings.find(featureName); const json* featureJsonView = (featureIt != settings.featureSettings.end()) ? &featureIt->second : nullptr; auto getFeatureJson = [&]() -> json& { @@ -1757,7 +1833,7 @@ void WeatherWidget::DrawFeatureSettings() ImGui::SetNextItemOpen(true); } - if (ImGui::TreeNodeEx(displayName.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) { + if (ImGui::TreeNodeEx(std::format("{}##{}", displayName, featureName).c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) { // Check if weather-specific overrides are enabled (using special key) bool overridesEnabled = featureJsonView ? featureJsonView->value("__enabled", false) : false; @@ -1766,18 +1842,18 @@ void WeatherWidget::DrawFeatureSettings() ImGui::PushStyleColor(ImGuiCol_ButtonHovered, overridesEnabled ? WidgetUI::kOverrideEnabledButtonHovered : WidgetUI::kOverrideDisabledButtonHovered); ImGui::PushStyleColor(ImGuiCol_ButtonActive, overridesEnabled ? WidgetUI::kOverrideEnabledButtonActive : WidgetUI::kOverrideDisabledButtonActive); - bool toggleClicked = ImGui::Button(overridesEnabled ? "Using Weather-Specific Settings" : "Using Global Settings", ImVec2(-1, 0)); + bool toggleClicked = ImGui::Button(overridesEnabled ? T(TKEY("using_weather_specific_settings"), "Using Weather-Specific Settings") : T(TKEY("using_global_settings"), "Using Global Settings"), ImVec2(-1, 0)); ImGui::PopStyleColor(3); if (auto _tt = Util::HoverTooltipWrapper()) { if (overridesEnabled) { - ImGui::Text("This weather has custom overrides for this feature."); - ImGui::Text("Click to disable overrides and use global settings instead."); - ImGui::Text("(Settings will be preserved but not applied)"); + ImGui::Text("%s", T(TKEY("custom_overrides_tooltip_0"), "This weather has custom overrides for this feature.")); + ImGui::Text("%s", T(TKEY("custom_overrides_tooltip_1"), "Click to disable overrides and use global settings instead.")); + ImGui::Text("%s", T(TKEY("custom_overrides_tooltip_2"), "(Settings will be preserved but not applied)")); } else { - ImGui::Text("This weather uses global feature settings."); - ImGui::Text("Click to enable weather-specific overrides."); + ImGui::Text("%s", T(TKEY("global_settings_tooltip_0"), "This weather uses global feature settings.")); + ImGui::Text("%s", T(TKEY("global_settings_tooltip_1"), "Click to enable weather-specific overrides.")); } } @@ -1866,7 +1942,7 @@ void WeatherWidget::DrawFeatureSettings() // Right-click context menu to reset individual values if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { + if (ImGui::MenuItem(T(TKEY("reset_to_global"), "Reset to Global"))) { featureJson.erase(varName); modified = true; } @@ -1889,7 +1965,7 @@ void WeatherWidget::DrawFeatureSettings() // Right-click context menu to reset individual values if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { + if (ImGui::MenuItem(T(TKEY("reset_to_global"), "Reset to Global"))) { featureJson.erase(varName); modified = true; } @@ -1911,7 +1987,7 @@ void WeatherWidget::DrawFeatureSettings() } if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { + if (ImGui::MenuItem(T(TKEY("reset_to_global"), "Reset to Global"))) { featureJson.erase(varName); modified = true; } @@ -1933,7 +2009,7 @@ void WeatherWidget::DrawFeatureSettings() } if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Reset to Global")) { + if (ImGui::MenuItem(T(TKEY("reset_to_global"), "Reset to Global"))) { featureJson.erase(varName); modified = true; } @@ -1944,10 +2020,10 @@ void WeatherWidget::DrawFeatureSettings() // Generic handling for other types ImGui::TextDisabled("%s: %s", varDisplayName.c_str(), currentValue.dump().c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::Text::Warning("Unsupported Variable Type"); + Util::Text::Warning("%s", T(TKEY("unsupported_variable_type"), "Unsupported Variable Type")); ImGui::Text("%s", tooltip.c_str()); ImGui::Separator(); - ImGui::TextWrapped("This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above."); + ImGui::TextWrapped("%s", T(TKEY("unsupported_variable_type_tooltip"), "This variable type doesn't have a custom UI implementation yet. The raw JSON value is shown above.")); } } @@ -1962,7 +2038,7 @@ void WeatherWidget::DrawFeatureSettings() } } else { - ImGui::TextColored(WidgetUI::kHelpTextColor, "Enable weather-specific overrides above to customize settings for this weather."); + ImGui::TextColored(WidgetUI::kHelpTextColor, "%s", T(TKEY("enable_weather_overrides_hint"), "Enable weather-specific overrides above to customize settings for this weather.")); } ImGui::TreePop(); @@ -1990,30 +2066,34 @@ std::vector WeatherWidget::CollectSearchableSettings() con { std::vector results; - const std::vector>> tabEntries = { - { WeatherTab::kBasic, { "Sun Damage", "Wind Speed", "Wind Direction", "Wind Direction Range", - "Precipitation Begin Fade In", "Precipitation End Fade Out", - "Thunder Lightning Begin Fade In", "Thunder Lightning End Fade Out", - "Thunder Lightning Frequency", "Lightning Color", - "Visual Effect Begin", "Visual Effect End", "Trans Delta" } }, - { WeatherTab::kFog, { WeatherSetting::kDayNear, WeatherSetting::kDayFar, WeatherSetting::kDayPower, WeatherSetting::kDayMax, - WeatherSetting::kNightNear, WeatherSetting::kNightFar, WeatherSetting::kNightPower, WeatherSetting::kNightMax } }, + const char* basicEntries[] = { + "Sun Damage", "Wind Speed", "Wind Direction", "Wind Direction Range", + "Precipitation Begin Fade In", "Precipitation End Fade Out", + "Thunder Lightning Begin Fade In", "Thunder Lightning End Fade Out", + "Thunder Lightning Frequency", "Lightning Color", + "Visual Effect Begin", "Visual Effect End", "Trans Delta" }; - - for (const auto& [tab, names] : tabEntries) { - for (const auto& name : names) { - results.push_back({ name, tab, name }); - } + for (const auto* name : basicEntries) { + results.push_back({ TranslateWeatherPropertyLabel(name), WeatherTab::kBasic, name }); } - results.push_back({ WeatherSetting::kFresnelPower, WeatherTab::kDalc, WeatherSetting::kFresnelPower }); - results.push_back({ WeatherSetting::kSpecular, WeatherTab::kDalc, WeatherSetting::kSpecular }); - results.push_back({ WeatherDisplay::kDirectionalXMax, WeatherTab::kDalc, WeatherSetting::kDirectionalXMax }); - results.push_back({ WeatherDisplay::kDirectionalXMin, WeatherTab::kDalc, WeatherSetting::kDirectionalXMin }); - results.push_back({ WeatherDisplay::kDirectionalYMax, WeatherTab::kDalc, WeatherSetting::kDirectionalYMax }); - results.push_back({ WeatherDisplay::kDirectionalYMin, WeatherTab::kDalc, WeatherSetting::kDirectionalYMin }); - results.push_back({ WeatherDisplay::kDirectionalZMax, WeatherTab::kDalc, WeatherSetting::kDirectionalZMax }); - results.push_back({ WeatherDisplay::kDirectionalZMin, WeatherTab::kDalc, WeatherSetting::kDirectionalZMin }); + results.push_back({ T(TKEY("day_near"), "Day Near"), WeatherTab::kFog, WeatherSetting::kDayNear }); + results.push_back({ T(TKEY("day_far"), "Day Far"), WeatherTab::kFog, WeatherSetting::kDayFar }); + results.push_back({ T(TKEY("day_power"), "Day Power"), WeatherTab::kFog, WeatherSetting::kDayPower }); + results.push_back({ T(TKEY("day_max"), "Day Max"), WeatherTab::kFog, WeatherSetting::kDayMax }); + results.push_back({ T(TKEY("night_near"), "Night Near"), WeatherTab::kFog, WeatherSetting::kNightNear }); + results.push_back({ T(TKEY("night_far"), "Night Far"), WeatherTab::kFog, WeatherSetting::kNightFar }); + results.push_back({ T(TKEY("night_power"), "Night Power"), WeatherTab::kFog, WeatherSetting::kNightPower }); + results.push_back({ T(TKEY("night_max"), "Night Max"), WeatherTab::kFog, WeatherSetting::kNightMax }); + + results.push_back({ T(TKEY("dalc_fresnel_power"), "Fresnel Power"), WeatherTab::kDalc, WeatherSetting::kFresnelPower }); + results.push_back({ T(TKEY("dalc_specular"), "Specular"), WeatherTab::kDalc, WeatherSetting::kSpecular }); + results.push_back({ T(TKEY("dalc_directional_x_max"), "Directional +X"), WeatherTab::kDalc, WeatherSetting::kDirectionalXMax }); + results.push_back({ T(TKEY("dalc_directional_x_min"), "Directional -X"), WeatherTab::kDalc, WeatherSetting::kDirectionalXMin }); + results.push_back({ T(TKEY("dalc_directional_y_max"), "Directional +Y"), WeatherTab::kDalc, WeatherSetting::kDirectionalYMax }); + results.push_back({ T(TKEY("dalc_directional_y_min"), "Directional -Y"), WeatherTab::kDalc, WeatherSetting::kDirectionalYMin }); + results.push_back({ T(TKEY("dalc_directional_z_max"), "Directional +Z"), WeatherTab::kDalc, WeatherSetting::kDirectionalZMax }); + results.push_back({ T(TKEY("dalc_directional_z_min"), "Directional -Z"), WeatherTab::kDalc, WeatherSetting::kDirectionalZMin }); for (int i = 0; i < ColorTypes::kTotal; i++) { std::string colorType = ColorTypeLabel(i); @@ -2021,21 +2101,21 @@ std::vector WeatherWidget::CollectSearchableSettings() con } for (int i = 0; i < TESWeather::kTotalLayers; i++) { - std::string layerId = std::format("Cloud Layer {}", i); + std::string layerId = std::vformat(T(TKEY("cloud_layer"), "Cloud Layer {}"), std::make_format_args(i)); results.push_back({ layerId, WeatherTab::kClouds, layerId }); } // Records tab: one entry per time-of-day slot for each form-picker section for (int i = 0; i < ColorTimes::kTotal; i++) { - std::string label = std::format("{} {}", WeatherRecord::kImageSpace, ColorTimeLabel(i)); + std::string label = std::format("{} {}", T(TKEY("record_imagespace"), "ImageSpace"), ColorTimeLabel(i)); results.push_back({ label, WeatherTab::kRecords, label }); } for (int i = 0; i < ColorTimes::kTotal; i++) { - std::string label = std::format("{} {}", WeatherRecord::kVolumetricLighting, ColorTimeLabel(i)); + std::string label = std::format("{} {}", T(TKEY("record_volumetric_lighting"), "Volumetric Lighting"), ColorTimeLabel(i)); results.push_back({ label, WeatherTab::kRecords, label }); } - results.push_back({ WeatherRecord::kPrecipitation, WeatherTab::kRecords, WeatherRecord::kPrecipitation }); - results.push_back({ WeatherRecord::kVisualEffect, WeatherTab::kRecords, WeatherRecord::kVisualEffect }); + results.push_back({ T(TKEY("record_precipitation"), "Precipitation"), WeatherTab::kRecords, WeatherRecord::kPrecipitation }); + results.push_back({ T(TKEY("record_visual_effect"), "Visual Effect"), WeatherTab::kRecords, WeatherRecord::kVisualEffect }); return results; } @@ -2051,6 +2131,8 @@ ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) return nullptr; } +#undef I18N_KEY_PREFIX + std::string resourcePath = WeatherUtils::TexturePath::BuildResourcePath(texturePath); ID3D11ShaderResourceView* srv = nullptr; diff --git a/src/CSEditor/WeatherUtils.cpp b/src/CSEditor/WeatherUtils.cpp index 93ce82fd85..7e6ce65c7a 100644 --- a/src/CSEditor/WeatherUtils.cpp +++ b/src/CSEditor/WeatherUtils.cpp @@ -1,9 +1,13 @@ #include "WeatherUtils.h" +#include "../I18n/I18n.h" #include "EditorWindow.h" #include "PaletteWindow.h" #include "Utils/FileSystem.h" #include "Utils/UI.h" +#define I18N_KEY_PREFIX "cs_editor." + +#include #include namespace WeatherUtils::TexturePath @@ -104,6 +108,156 @@ static std::string_view UnscopeKey(std::string_view key) return pos == std::string_view::npos ? key : key.substr(pos + kScopeSep.size()); } +const char* WeatherUtils::TranslateControlLabel(std::string_view label) +{ + if (label == "Ambient Color") + return T(TKEY("ambient_color"), "Ambient Color"); + if (label == "Directional Color") + return T(TKEY("directional_color"), "Directional Color"); + if (label == "Directional XY") + return T(TKEY("directional_xy"), "Directional XY"); + if (label == "Directional Z") + return T(TKEY("directional_z"), "Directional Z"); + if (label == "Directional Fade") + return T(TKEY("directional_fade"), "Directional Fade"); + if (label == "Light Fade Start") + return T(TKEY("light_fade_start"), "Light Fade Start"); + if (label == "Light Fade End") + return T(TKEY("light_fade_end"), "Light Fade End"); + if (label == "Clip Distance") + return T(TKEY("clip_distance"), "Clip Distance"); + if (label == "Fog Color Near" || label == "Fog Near Color") + return T(TKEY("fog_color_near"), "Fog Color Near"); + if (label == "Fog Color Far" || label == "Fog Far Color") + return T(TKEY("fog_color_far"), "Fog Color Far"); + if (label == "Fog Near") + return T(TKEY("color_fog_near"), "Fog Near"); + if (label == "Fog Far") + return T(TKEY("color_fog_far"), "Fog Far"); + if (label == "Fog Power") + return T(TKEY("fog_power"), "Fog Power"); + if (label == "Fog Clamp" || label == "Fog Clamp (Max)") + return T(TKEY("fog_clamp"), "Fog Clamp"); + if (label == "Specular") + return T(TKEY("dalc_specular"), "Specular"); + if (label == "Fresnel Power") + return T(TKEY("dalc_fresnel_power"), "Fresnel Power"); + if (label == "X+ (Right)") + return T(TKEY("direction_x_plus"), "X+ (Right)"); + if (label == "X- (Left)") + return T(TKEY("direction_x_minus"), "X- (Left)"); + if (label == "Y+ (Front)") + return T(TKEY("direction_y_plus"), "Y+ (Front)"); + if (label == "Y- (Back)") + return T(TKEY("direction_y_minus"), "Y- (Back)"); + if (label == "Z+ (Up)") + return T(TKEY("direction_z_plus"), "Z+ (Up)"); + if (label == "Z- (Down)") + return T(TKEY("direction_z_minus"), "Z- (Down)"); + if (label == "XY Rotation") + return T(TKEY("xy_rotation"), "XY Rotation"); + if (label == "Z Rotation") + return T(TKEY("z_rotation"), "Z Rotation"); + if (label == "Inherit Ambient Color") + return T(TKEY("inherit_ambient_color"), "Inherit Ambient Color"); + if (label == "Inherit Directional Color") + return T(TKEY("inherit_directional_color"), "Inherit Directional Color"); + if (label == "Inherit Fog Color") + return T(TKEY("inherit_fog_color"), "Inherit Fog Color"); + if (label == "Inherit Fog Near") + return T(TKEY("inherit_fog_near"), "Inherit Fog Near"); + if (label == "Inherit Fog Far") + return T(TKEY("inherit_fog_far"), "Inherit Fog Far"); + if (label == "Inherit Directional Rotation") + return T(TKEY("inherit_directional_rotation"), "Inherit Directional Rotation"); + if (label == "Inherit Directional Fade") + return T(TKEY("inherit_directional_fade"), "Inherit Directional Fade"); + if (label == "Inherit Clip Distance") + return T(TKEY("inherit_clip_distance"), "Inherit Clip Distance"); + if (label == "Inherit Fog Power") + return T(TKEY("inherit_fog_power"), "Inherit Fog Power"); + if (label == "Inherit Fog Max (Clamp)") + return T(TKEY("inherit_fog_max_clamp"), "Inherit Fog Max (Clamp)"); + if (label == "Inherit Light Fade Distances") + return T(TKEY("inherit_light_fade_distances"), "Inherit Light Fade Distances"); + if (label == "Type") + return T(TKEY("type"), "Type"); + if (label == "Size X") + return T(TKEY("size_x"), "Size X"); + if (label == "Size Y") + return T(TKEY("size_y"), "Size Y"); + if (label == "Gravity Velocity") + return T(TKEY("gravity_velocity"), "Gravity Velocity"); + if (label == "Rotation Velocity") + return T(TKEY("rotation_velocity"), "Rotation Velocity"); + if (label == "Center Offset Min") + return T(TKEY("center_offset_min"), "Center Offset Min"); + if (label == "Center Offset Max") + return T(TKEY("center_offset_max"), "Center Offset Max"); + if (label == "Start Rotation Range") + return T(TKEY("start_rotation_range"), "Start Rotation Range"); + if (label == "Box Size") + return T(TKEY("box_size"), "Box Size"); + if (label == "Particle Density") + return T(TKEY("particle_density_label"), "Particle Density"); + if (label == "Num Subtextures X") + return T(TKEY("num_subtextures_x"), "Num Subtextures X"); + if (label == "Num Subtextures Y") + return T(TKEY("num_subtextures_y"), "Num Subtextures Y"); + if (label == "Particle Texture") + return T(TKEY("particle_texture_label"), "Particle Texture"); + if (label == "Art Object") + return T(TKEY("art_object"), "Art Object"); + if (label == "Effect Shader") + return T(TKEY("effect_shader"), "Effect Shader"); + if (label == "Face Target") + return T(TKEY("face_target"), "Face Target"); + if (label == "Attach To Camera") + return T(TKEY("attach_to_camera"), "Attach To Camera"); + if (label == "Inherit Rotation") + return T(TKEY("inherit_rotation"), "Inherit Rotation"); + if (label == "Intensity") + return T(TKEY("intensity"), "Intensity"); + if (label == "Contribution") + return T(TKEY("contribution"), "Contribution"); + if (label == "Color") + return T(TKEY("color"), "Color"); + if (label == "Size") + return T(TKEY("size"), "Size"); + if (label == "Wind Speed") + return T(TKEY("wind_speed"), "Wind Speed"); + if (label == "Falling Speed") + return T(TKEY("falling_speed"), "Falling Speed"); + if (label == "Scattering") + return T(TKEY("scattering"), "Scattering"); + if (label == "Range Factor") + return T(TKEY("range_factor"), "Range Factor"); + if (label == "Cloud Layer Speed X") + return T(TKEY("cloud_layer_speed_x"), "Cloud Layer Speed X"); + if (label == "Cloud Layer Speed Y") + return T(TKEY("cloud_layer_speed_y"), "Cloud Layer Speed Y"); + + // Fallback: return the original label via T() which caches a stable null-terminated copy + return T(std::string(label).c_str(), std::string(label).c_str()); +} + +static std::string BuildLocalizedControlLabel(const std::string& label) +{ + if (label.starts_with("##")) + return label; + + const auto idPos = label.find("##"); + const auto visible = idPos == std::string::npos ? label : label.substr(0, idPos); + const auto id = idPos == std::string::npos ? visible : label.substr(idPos + 2); + const bool numericId = !id.empty() && std::all_of(id.begin(), id.end(), [](unsigned char c) { return std::isdigit(c); }); + const auto key = numericId ? visible : id; + const auto* translated = WeatherUtils::TranslateControlLabel(key); + if (std::string_view(translated) == key) + return label; + + return std::format("{}##{}", translated, id); +} + // Per-widget-type window sizes — shared across all instances of the same widget type static std::unordered_map s_widgetTypeSizes; @@ -227,16 +381,16 @@ std::string ColorTimeLabel(const int i) std::string label = ""; switch (i) { case 0: - label = "Sunrise"; + label = T(TKEY("tod_sunrise"), "Sunrise"); break; case 1: - label = "Day"; + label = T(TKEY("tod_day"), "Day"); break; case 2: - label = "Sunset"; + label = T(TKEY("tod_sunset"), "Sunset"); break; case 3: - label = "Night"; + label = T(TKEY("tod_night"), "Night"); break; default: break; @@ -249,55 +403,55 @@ std::string ColorTypeLabel(const int i) std::string label = ""; switch (i) { case 0: - label = "Sky Upper"; + label = T(TKEY("color_sky_upper"), "Sky Upper"); break; case 1: - label = "Fog Near"; + label = T(TKEY("color_fog_near"), "Fog Near"); break; case 2: - label = "Unknown"; + label = T(TKEY("unknown"), "Unknown"); break; case 3: - label = "Ambient"; + label = T(TKEY("color_ambient"), "Ambient"); break; case 4: - label = "Sunlight"; + label = T(TKEY("color_sunlight"), "Sunlight"); break; case 5: - label = "Sun"; + label = T(TKEY("color_sun"), "Sun"); break; case 6: - label = "Stars"; + label = T(TKEY("color_stars"), "Stars"); break; case 7: - label = "Sky Lower"; + label = T(TKEY("color_sky_lower"), "Sky Lower"); break; case 8: - label = "Horizon"; + label = T(TKEY("color_horizon"), "Horizon"); break; case 9: - label = "Effect Lighting"; + label = T(TKEY("color_effect_lighting"), "Effect Lighting"); break; case 10: - label = "Cloud LOD Diffuse"; + label = T(TKEY("color_cloud_lod_diffuse"), "Cloud LOD Diffuse"); break; case 11: - label = "Cloud LOD Ambient"; + label = T(TKEY("color_cloud_lod_ambient"), "Cloud LOD Ambient"); break; case 12: - label = "Fog Far"; + label = T(TKEY("color_fog_far"), "Fog Far"); break; case 13: - label = "Sky Statics"; + label = T(TKEY("color_sky_statics"), "Sky Statics"); break; case 14: - label = "Water Multiplier"; + label = T(TKEY("color_water_multiplier"), "Water Multiplier"); break; case 15: - label = "Sun Glare"; + label = T(TKEY("color_sun_glare"), "Sun Glare"); break; case 16: - label = "Moon Glare"; + label = T(TKEY("color_moon_glare"), "Moon Glare"); break; default: break; @@ -320,9 +474,10 @@ namespace WeatherUtils { const double debounceDelay = 2.0; double currentTime = ImGui::GetTime(); + const auto displayLabel = BuildLocalizedControlLabel(label); bool changed = DrawWithWidgetHighlight(g_currentWidget, label, [&]() { - return ImGui::SliderInt(label.c_str(), &property, -127, 127); + return ImGui::SliderInt(displayLabel.c_str(), &property, -127, 127); }); bool isNowActive = ImGui::IsItemActive(); @@ -364,7 +519,8 @@ namespace WeatherUtils const std::string cacheId = effectiveWidget ? std::format("{}{}{}", static_cast(effectiveWidget), kScopeSep, hid) : hid; - bool isActive = ImGui::IsPopupOpen(l.c_str(), ImGuiPopupFlags_AnyPopupId); + const auto displayLabel = BuildLocalizedControlLabel(l); + bool isActive = ImGui::IsPopupOpen(displayLabel.c_str(), ImGuiPopupFlags_AnyPopupId); bool wasActive = wasPickerOpen[cacheId]; // Cache the original color and push undo state when picker is first activated @@ -389,7 +545,7 @@ namespace WeatherUtils } bool changed = DrawWithWidgetHighlight(effectiveWidget, hid, [&]() { - return ImGui::ColorEdit3(l.c_str(), (float*)&property); + return ImGui::ColorEdit3(displayLabel.c_str(), (float*)&property); }); // Track color usage only when picker closes @@ -423,8 +579,9 @@ namespace WeatherUtils bool DrawSliderUint8(const std::string& label, int& property) { + const auto displayLabel = BuildLocalizedControlLabel(label); bool changed = DrawWithWidgetHighlight(g_currentWidget, label, [&]() { - return ImGui::SliderInt(label.c_str(), &property, 0, 255); + return ImGui::SliderInt(displayLabel.c_str(), &property, 0, 255); }); return changed; } @@ -436,12 +593,13 @@ namespace WeatherUtils // Strip leading "##" so hidden-label sliders still match highlight/search ids. std::string hid = label.starts_with("##") ? label.substr(2) : label; + const auto displayLabel = BuildLocalizedControlLabel(label); Widget* w = widget ? widget : g_currentWidget; if (w && !w->MatchesSearch(hid)) return false; bool changed = DrawWithWidgetHighlight(w, hid, [&]() { - return ImGui::SliderFloat(label.c_str(), &property, min, max, format); + return ImGui::SliderFloat(displayLabel.c_str(), &property, min, max, format); }); bool isNowActive = ImGui::IsItemActive(); @@ -473,11 +631,12 @@ namespace WeatherUtils { Widget* w = widget ? widget : g_currentWidget; const std::string hid = label.starts_with("##") ? label.substr(2) : label; + const auto displayLabel = BuildLocalizedControlLabel(label); if (w && !w->MatchesSearch(hid)) return false; return DrawWithWidgetHighlight(w, hid, [&]() { - return ImGui::Checkbox(label.c_str(), &value); + return ImGui::Checkbox(displayLabel.c_str(), &value); }); } } @@ -487,10 +646,18 @@ namespace TOD { const char* GetPeriodName(int index) { - static const char* names[Count] = { "Sunrise", "Day", "Sunset", "Night" }; - if (index >= 0 && index < Count) - return names[index]; - return "Unknown"; + switch (index) { + case Sunrise: + return T(TKEY("tod_sunrise"), "Sunrise"); + case Day: + return T(TKEY("tod_day"), "Day"); + case Sunset: + return T(TKEY("tod_sunset"), "Sunset"); + case Night: + return T(TKEY("tod_night"), "Night"); + default: + return T(TKEY("unknown"), "Unknown"); + } } float GetCurrentGameTime() @@ -841,7 +1008,7 @@ namespace TOD changed = true; } } - Util::AddTooltip("Inherit from parent"); + Util::AddTooltip(T(TKEY("inherit_from_parent"), "Inherit from parent")); ImGui::PopStyleVar(); ImGui::SameLine(0, 2 * scale); } @@ -882,7 +1049,7 @@ namespace TOD ImGui::EndDisabled(); - Util::AddTooltip(isInherited ? "Inherited from parent weather" : std::format("{:.0f}%", factors[i] * 100.0f).c_str()); + Util::AddTooltip(isInherited ? T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather") : std::format("{:.0f}%", factors[i] * 100.0f).c_str()); ImGui::PopItemWidth(); if (isInherited) @@ -949,7 +1116,7 @@ namespace TOD ImGui::PopStyleVar(); ImGui::PopStyleColor(2); - Util::AddTooltip("Inherit from parent weather"); + Util::AddTooltip(T(TKEY("inherit_from_parent_weather"), "Inherit from parent weather")); } if (!anyActive) @@ -1061,7 +1228,7 @@ namespace TOD ImGui::EndDisabled(); if (inheritFlag) { - Util::AddTooltip("Inherited from parent weather"); + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); PopInheritedStyle(); } @@ -1149,7 +1316,7 @@ namespace TOD ImGui::PopStyleVar(); ImGui::PopStyleColor(2); - Util::AddTooltip("Inherit from parent weather"); + Util::AddTooltip(T(TKEY("inherit_from_parent_weather"), "Inherit from parent weather")); } ImGui::TableSetColumnIndex(1); @@ -1177,7 +1344,7 @@ namespace TOD changed = true; } if (inheritFlag) - Util::AddTooltip("Inherited from parent weather"); + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); ImGui::PopID(); } ImGui::EndDisabled(); @@ -1234,8 +1401,8 @@ namespace TOD if (paramColumnWidth <= 0.0f) paramColumnWidth = WidgetDefaults::kTODLabelWidth; if (ImGui::BeginTable(tableId, 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, paramColumnWidth * Util::GetUIScale()); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T(TKEY("parameter"), "Parameter"), ImGuiTableColumnFlags_WidthFixed, paramColumnWidth * Util::GetUIScale()); + ImGui::TableSetupColumn(T(TKEY("value"), "Value"), ImGuiTableColumnFlags_WidthStretch); return true; } return false; @@ -1270,8 +1437,8 @@ namespace PropertyDrawer bool BeginTable(const char* tableId, float labelWidth) { if (ImGui::BeginTable(tableId, 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, labelWidth * Util::GetUIScale()); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T(TKEY("parameter"), "Parameter"), ImGuiTableColumnFlags_WidthFixed, labelWidth * Util::GetUIScale()); + ImGui::TableSetupColumn(T(TKEY("value"), "Value"), ImGuiTableColumnFlags_WidthStretch); return true; } return false; @@ -1342,3 +1509,5 @@ namespace PropertyDrawer }); } } // namespace PropertyDrawer + +#undef I18N_KEY_PREFIX diff --git a/src/CSEditor/WeatherUtils.h b/src/CSEditor/WeatherUtils.h index 8b238a9a16..f580f96c0c 100644 --- a/src/CSEditor/WeatherUtils.h +++ b/src/CSEditor/WeatherUtils.h @@ -1,8 +1,10 @@ #pragma once +#include "../I18n/I18n.h" #include "Util.h" #include "Widget.h" #include +#include #include #include #include @@ -170,6 +172,29 @@ namespace PropertyDrawer // ============================================================================ namespace WidgetFactory { + inline const char* TranslateWidgetTypeName(std::string_view widgetTypeName) + { + if (widgetTypeName == "Weather") + return T("cs_editor.widget_type_weather", "Weather"); + if (widgetTypeName == "ImageSpace") + return T("cs_editor.widget_type_imagespace", "ImageSpace"); + if (widgetTypeName == "Lighting") + return T("cs_editor.widget_type_lighting", "Lighting"); + if (widgetTypeName == "Cell Lighting") + return T("cs_editor.widget_type_cell_lighting", "Cell Lighting"); + if (widgetTypeName == "Volumetric Lighting") + return T("cs_editor.widget_type_volumetric_lighting", "Volumetric Lighting"); + if (widgetTypeName == "Precipitation") + return T("cs_editor.widget_type_precipitation", "Precipitation"); + if (widgetTypeName == "Lens Flare") + return T("cs_editor.widget_type_lens_flare", "Lens Flare"); + if (widgetTypeName == "Visual Effect") + return T("cs_editor.widget_type_visual_effect", "Visual Effect"); + + // Fallback: use T() to cache a stable null-terminated copy + return T(std::string(widgetTypeName).c_str(), std::string(widgetTypeName).c_str()); + } + // Populate a widget container from a form array // WidgetType must have a constructor taking FormType* template @@ -234,7 +259,7 @@ namespace WidgetFactory for (auto& widget : widgets) { if (widget->IsOpen()) { ++count; - if (ImGui::MenuItem(std::format("{}: {}", widget->GetWidgetTypeName(), widget->GetEditorID()).c_str())) + if (ImGui::MenuItem(std::format("{}: {}", TranslateWidgetTypeName(widget->GetWidgetTypeName()), widget->GetEditorID()).c_str())) ImGui::SetWindowFocus(widget->GetWindowTitle().c_str()); } } @@ -248,7 +273,8 @@ namespace WidgetFactory for (auto& widget : widgets) { if (widget->IsOpen()) { hasOpen = true; - if (ImGui::MenuItem(std::format("Save {}", widget->GetEditorID()).c_str())) + auto editorId = widget->GetEditorID(); + if (ImGui::MenuItem(std::vformat(T("cs_editor.save_widget", "Save {}"), std::make_format_args(editorId)).c_str())) widget->Save(); } } @@ -261,7 +287,8 @@ namespace WidgetFactory { if (widgets.empty()) return; - if (ImGui::MenuItem(std::format("Close All {} Widgets", widgets[0]->GetWidgetTypeName()).c_str())) { + auto typeName = TranslateWidgetTypeName(widgets[0]->GetWidgetTypeName()); + if (ImGui::MenuItem(std::vformat(T("cs_editor.close_all_widgets", "Close All {} Widgets"), std::make_format_args(typeName)).c_str())) { for (auto& widget : widgets) widget->SetOpen(false); } @@ -377,6 +404,7 @@ namespace WeatherUtils void SetCurrentWidget(Widget* widget); // UI helper functions + const char* TranslateControlLabel(std::string_view label); bool DrawSliderInt8(const std::string& label, int& property); bool DrawColorEdit(const std::string& l, float3& property, Widget* widget = nullptr); bool DrawSliderUint8(const std::string& label, int& property); @@ -402,14 +430,14 @@ namespace WeatherUtils std::format("{} (0x{:08X})", effectiveID, currentForm->GetFormID()) : effectiveID; } else { - previewText = "None"; + previewText = ::T("cs_editor.none_filter", "None"); } if (width > 0.0f) ImGui::SetNextItemWidth(width); if (ImGui::BeginCombo(label, previewText.c_str())) { - if (allowNone && ImGui::Selectable("None", currentForm == nullptr)) { + if (allowNone && ImGui::Selectable(::T("cs_editor.none_filter", "None"), currentForm == nullptr)) { currentForm = nullptr; changed = true; } diff --git a/src/CSEditor/Widget.cpp b/src/CSEditor/Widget.cpp index 0f34fae9e8..693fd61f9d 100644 --- a/src/CSEditor/Widget.cpp +++ b/src/CSEditor/Widget.cpp @@ -3,6 +3,7 @@ #include #include +#include "../I18n/I18n.h" #include "EditorWindow.h" #include "State.h" #include "Util.h" @@ -10,6 +11,8 @@ #include "WeatherUtils.h" #include "imgui_internal.h" +#define I18N_KEY_PREFIX "cs_editor." + void Widget::Save() { SaveSettings(); @@ -190,17 +193,17 @@ bool Widget::HasSavedFile() const void Widget::DrawMenu() { if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("Menu")) { - if (ImGui::MenuItem("Save")) { + if (ImGui::BeginMenu(T(TKEY("menu"), "Menu"))) { + if (ImGui::MenuItem(T(TKEY("save"), "Save"))) { Save(); } - if (ImGui::MenuItem("Load")) { + if (ImGui::MenuItem(T(TKEY("load"), "Load"))) { Load(); } - if (ImGui::MenuItem("Delete Saved File")) { + if (ImGui::MenuItem(T(TKEY("delete_saved_file"), "Delete Saved File"))) { ImGui::OpenPopup("DeleteConfirmation"); } - if (ImGui::MenuItem("Revert to Game Values")) { + if (ImGui::MenuItem(T(TKEY("revert_to_game_values"), "Revert to Game Values"))) { RevertChanges(); } @@ -221,7 +224,7 @@ void Widget::DrawDeleteConfirmationModal(const char* popupId) if (auto popup = Util::CenteredPopupModal(popupId)) { deleteConfirmationFrame = ImGui::GetFrameCount(); - ImGui::Text("Are you sure you want to delete the saved settings file?"); + ImGui::Text("%s", T(TKEY("confirm_delete_saved_file"), "Are you sure you want to delete the saved settings file?")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -234,13 +237,13 @@ void Widget::DrawDeleteConfirmationModal(const char* popupId) ImGui::SetCursorPosX(cursorX); - if (ImGui::Button("Yes, Delete", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T(TKEY("yes_delete"), "Yes, Delete"), ImVec2(buttonWidth, 0))) { Delete(); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T(TKEY("cancel"), "Cancel"), ImVec2(buttonWidth, 0))) { ImGui::CloseCurrentPopup(); } ImGui::SetItemDefaultFocus(); @@ -319,7 +322,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav ClearSearchState(true); ImGui::SetKeyboardFocusHere(); } - ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer)); + ImGui::InputTextWithHint(searchId, T(TKEY("search_settings_hint"), "Search settings (Ctrl+F)"), searchBuffer, sizeof(searchBuffer)); searchInputMin = ImGui::GetItemRectMin(); searchInputMax = ImGui::GetItemRectMax(); if (ImGui::IsItemEdited()) @@ -331,7 +334,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav return; ImGui::SameLine(); bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; - const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; + const char* lockLabel = isLocked ? T(TKEY("unlock"), "Unlock") : T(TKEY("force_weather"), "Force Weather"); if (isLocked) { ImGui::PushStyleColor(ImGuiCol_Button, WidgetUI::kLockButtonColor); @@ -345,15 +348,15 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav } if (isLocked) ImGui::PopStyleColor(2); - Util::AddTooltip(isLocked ? "Unlock Weather" : "Force This Weather"); + Util::AddTooltip(isLocked ? T(TKEY("unlock_weather"), "Unlock Weather") : T(TKEY("force_this_weather"), "Force This Weather")); }; auto drawUnsavedIndicator = [&]() { if (!HasUnsavedChanges() || !menu) return; ImGui::SameLine(); - ImGui::TextColored(menu->GetTheme().StatusPalette.Warning, "(UNSAVED CHANGES)"); - Util::AddTooltip("Unsaved changes - click save to keep"); + ImGui::TextColored(menu->GetTheme().StatusPalette.Warning, "%s", T(TKEY("unsaved_changes"), "(UNSAVED CHANGES)")); + Util::AddTooltip(T(TKEY("unsaved_changes_tooltip"), "Unsaved changes - click save to keep")); }; if (useIcons) { @@ -382,26 +385,26 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav // Apply button if (showApply && (!editorWindow->settings.autoApplyChanges || RequiresManualApply())) { if (menu->uiIcons.applyToGame.texture) { - iconButton("_Apply", menu->uiIcons.applyToGame.texture, "Apply changes to the game", [&]() { ApplyChanges(); }); + iconButton("_Apply", menu->uiIcons.applyToGame.texture, T(TKEY("apply_changes"), "Apply changes to the game"), [&]() { ApplyChanges(); }); } else { ImGui::SameLine(); - if (ImGui::Button("Apply")) + if (ImGui::Button(T(TKEY("apply"), "Apply"))) ApplyChanges(); - Util::AddTooltip("Apply changes to the game"); + Util::AddTooltip(T(TKEY("apply_changes"), "Apply changes to the game")); } } // Save/Load/Revert/Delete group if (showSaveLoadRevert) { - iconButton("_Save", menu->uiIcons.saveSettings.texture, "Save to file", [&]() { Save(); }); - iconButton("_Load", menu->uiIcons.loadSettings.texture, "Load saved file (or reset to vanilla if no file)", [&]() { Load(); }); - iconButton("_Revert", menu->uiIcons.featureSettingRevert.texture, "Revert to original game values", [&]() { RevertChanges(); }); + iconButton("_Save", menu->uiIcons.saveSettings.texture, T(TKEY("save_to_file"), "Save to file"), [&]() { Save(); }); + iconButton("_Load", menu->uiIcons.loadSettings.texture, T(TKEY("load_saved_file"), "Load saved file (or reset to vanilla if no file)"), [&]() { Load(); }); + iconButton("_Revert", menu->uiIcons.featureSettingRevert.texture, T(TKEY("revert_to_original"), "Revert to original game values"), [&]() { RevertChanges(); }); if (HasSavedFile() && menu->uiIcons.deleteSettings.texture) { ImGui::SameLine(); if (Util::ErrorImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize)) ImGui::OpenPopup("DeleteConfirmation"); - Util::AddTooltip("Delete saved file"); + Util::AddTooltip(T(TKEY("delete_saved_file_tooltip"), "Delete saved file")); } } @@ -426,25 +429,25 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav // Apply button if (showApply && (!editorWindow->settings.autoApplyChanges || RequiresManualApply())) { ImGui::SameLine(); - if (Util::SuccessButton("Apply")) + if (Util::SuccessButton(T(TKEY("apply"), "Apply"))) ApplyChanges(); - Util::AddTooltip("Apply changes to the game"); + Util::AddTooltip(T(TKEY("apply_changes"), "Apply changes to the game")); } // Save/Load/Revert/Delete group if (showSaveLoadRevert) { - textButton("Save", "Save to file", [&]() { Save(); }); - textButton("Load", "Load saved file (or reset to vanilla if no file)", [&]() { Load(); }); + textButton(T(TKEY("save"), "Save"), T(TKEY("save_to_file"), "Save to file"), [&]() { Save(); }); + textButton(T(TKEY("load"), "Load"), T(TKEY("load_saved_file"), "Load saved file (or reset to vanilla if no file)"), [&]() { Load(); }); ImGui::SameLine(); - if (Util::WarningButton("Revert")) + if (Util::WarningButton(T(TKEY("revert"), "Revert"))) RevertChanges(); - Util::AddTooltip("Revert to original game values"); + Util::AddTooltip(T(TKEY("revert_to_original"), "Revert to original game values")); if (HasSavedFile()) { ImGui::SameLine(); - if (Util::ErrorTextButton("Delete")) + if (Util::ErrorTextButton(T(TKEY("delete"), "Delete"))) ImGui::OpenPopup("DeleteConfirmation"); - Util::AddTooltip("Delete saved file"); + Util::AddTooltip(T(TKEY("delete_saved_file_tooltip"), "Delete saved file")); } } @@ -456,8 +459,8 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApply, bool showSav if (showApply && RequiresManualApply() && editorWindow->settings.autoApplyChanges && menu) { ImGui::SameLine(); - ImGui::TextColored(menu->GetTheme().StatusPalette.Warning, "(Changes require manual apply)"); - Util::AddTooltip("This form type is only re-read by the engine on weather reinit.\nAuto-apply is disabled - use the Apply button."); + ImGui::TextColored(menu->GetTheme().StatusPalette.Warning, "%s", T(TKEY("changes_require_manual_apply"), "(Changes require manual apply)")); + Util::AddTooltip(T(TKEY("manual_apply_required_tooltip"), "This form type is only re-read by the engine on weather reinit.\nAuto-apply is disabled - use the Apply button.")); } ImGui::Separator(); @@ -525,7 +528,9 @@ void Widget::DrawSearchDropdown() if (searchResults.size() > WidgetUI::kSearchDropdownMaxResults) { ImGui::Separator(); - ImGui::TextDisabled("... %zu more results", searchResults.size() - WidgetUI::kSearchDropdownMaxResults); + auto count = searchResults.size() - WidgetUI::kSearchDropdownMaxResults; + auto formatted = std::vformat(T(TKEY("more_results"), "... {} more results"), std::make_format_args(count)); + ImGui::TextDisabled("%s", formatted.c_str()); } } } @@ -609,3 +614,5 @@ void Widget::PopHighlightStyle(const std::string& settingId) scrollToHighlighted = false; } } + +#undef I18N_KEY_PREFIX diff --git a/src/Feature.cpp b/src/Feature.cpp index 9cfad1278e..6fb92d66de 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -2,6 +2,7 @@ #include "FeatureIssues.h" #include "FeatureVersions.h" +#include "Features/CSEditor.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" #include "Features/ExponentialHeightFog.h" @@ -36,8 +37,8 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/CSEditor.h" #include "Features/WetnessEffects.h" +#include "I18n/I18n.h" #include "Menu.h" #include "SettingsOverrideManager.h" #include "Utils/Format.h" @@ -338,6 +339,33 @@ bool Feature::ReapplyOverrideSettings() return false; } +std::string Feature::GetDisplayCategory() const +{ + const auto category = GetCategory(); + if (category == FeatureCategories::kCharacters) + return T("feature.category.characters", "Characters"); + if (category == FeatureCategories::kDisplay) + return T("feature.category.display", "Display"); + if (category == FeatureCategories::kGrass) + return T("feature.category.grass", "Grass"); + if (category == FeatureCategories::kLandscapeAndTextures) + return T("feature.category.landscape_and_textures", "Landscape & Textures"); + if (category == FeatureCategories::kLighting) + return T("feature.category.lighting", "Lighting"); + if (category == FeatureCategories::kMaterials) + return T("feature.category.materials", "Materials"); + if (category == FeatureCategories::kOther) + return T("feature.category.other", "Other"); + if (category == FeatureCategories::kSky) + return T("feature.category.sky", "Sky"); + if (category == FeatureCategories::kUtility) + return T("feature.category.utility", "Utility"); + if (category == FeatureCategories::kWater) + return T("feature.category.water", "Water"); + + return std::string(category); +} + void Feature::DrawUnloadedUI() { // Prioritize detailed failure message if available @@ -368,7 +396,7 @@ void Feature::DrawUnloadedUI() if (description.empty()) { ImGui::Spacing(); } - ImGui::TextWrapped("Key features:"); + ImGui::TextWrapped("%s", T("feature.key_features", "Key features:")); for (const auto& feature : keyFeatures) { ImGui::BulletText("%s", feature.c_str()); } diff --git a/src/Feature.h b/src/Feature.h index 95ca1741a7..adc3ba32fb 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -3,6 +3,7 @@ #include "FeatureCategories.h" #include "FeatureConstraints.h" #include "FeatureVersions.h" +#include "I18n/I18n.h" #ifdef TRACY_ENABLE # include # include @@ -29,6 +30,8 @@ struct Feature virtual std::string GetName() = 0; virtual std::string GetShortName() = 0; + virtual std::string GetDisplayName() { return GetName(); } + std::string GetDisplayCategory() const; virtual std::string GetFeatureModLink() { return ""; } virtual std::string_view GetShaderDefineName() { return ""; } virtual std::vector> GetShaderDefineOptions() { return {}; } diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index f954c88736..3e8ad91a53 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1,6 +1,7 @@ #include "FeatureIssues.h" #include "Feature.h" +#include "I18n/I18n.h" #include "Menu.h" #include "State.h" #include "Util.h" @@ -345,8 +346,8 @@ namespace FeatureIssues const auto& featureIssues = GetFeatureIssues(); if (featureIssues.empty()) { - ImGui::TextWrapped("No feature issues found!"); - ImGui::TextWrapped("All feature INI files are loading successfully."); + ImGui::TextWrapped("%s", T("menu.issues.no_issues", "No feature issues found!")); + ImGui::TextWrapped("%s", T("menu.issues.all_ini_loading", "All feature INI files are loading successfully.")); return; } @@ -373,44 +374,49 @@ namespace FeatureIssues } } // Shader Breaking Features Section (most critical) - if (auto section = Util::SectionWrapper("Compilation Breaking Features", - "The following features modified core shader files and must be completely uninstalled via your mod manager. " - "Deleting just the INI file will not fix compilation errors if core shaders were modified.", + if (auto section = Util::SectionWrapper(T("menu.issues.compilation_breaking_header", "Compilation Breaking Features"), + T("menu.issues.compilation_breaking_desc", + "The following features modified core shader files and must be completely uninstalled via your mod manager. " + "Deleting just the INI file will not fix compilation errors if core shaders were modified."), theme.StatusPalette.Error, !shaderBreakingIssues.empty())) { for (const auto* issue : shaderBreakingIssues) { DrawFeatureIssue(*issue, theme.StatusPalette.Error); } } // Unknown Features Section (potentially compilation breaking) - if (auto section = Util::SectionWrapper("Unknown Features", - "The following features are not recognized and we tried to disable automatically. " - "They may be from development branches or newer CS versions. Since we cannot determine what files they may have modified, " - "they should be removed as a precaution to prevent potential shader compilation failures.", + if (auto section = Util::SectionWrapper(T("menu.issues.unknown_features_header", "Unknown Features"), + T("menu.issues.unknown_features_desc", + "The following features are not recognized and we tried to disable automatically. " + "They may be from development branches or newer CS versions. Since we cannot determine what files they may have modified, " + "they should be removed as a precaution to prevent potential shader compilation failures."), theme.StatusPalette.Error, !unknownIssues.empty())) { for (const auto* issue : unknownIssues) { DrawFeatureIssue(*issue, theme.StatusPalette.Error); } } // Obsolete Features Section (non-shader-breaking) - if (auto section = Util::SectionWrapper("Obsolete Features", - "The following features are obsolete and disabled automatically. " - "These features have been removed or replaced in this CS version but do not modify core shaders.", + if (auto section = Util::SectionWrapper(T("menu.issues.obsolete_features_header", "Obsolete Features"), + T("menu.issues.obsolete_features_desc", + "The following features are obsolete and disabled automatically. " + "These features have been removed or replaced in this CS version but do not modify core shaders."), theme.StatusPalette.Warning, !obsoleteIssues.empty())) { for (const auto* issue : obsoleteIssues) { DrawFeatureIssue(*issue, theme.StatusPalette.Warning); } } // Version Mismatch Section - if (auto section = Util::SectionWrapper("Wrong Version Features", - "The following features have version compatibility issues and were disabled automatically. Please check for any updates or if the feature is considered obsolete.", + if (auto section = Util::SectionWrapper(T("menu.issues.wrong_version_header", "Wrong Version Features"), + T("menu.issues.wrong_version_desc", + "The following features have version compatibility issues and were disabled automatically. Please check for any updates or if the feature is considered obsolete."), theme.StatusPalette.Warning, !versionIssues.empty())) { for (const auto* issue : versionIssues) { DrawFeatureIssue(*issue, theme.StatusPalette.Warning); } } // Override Failures Section - if (auto section = Util::SectionWrapper("Override Failures", - "The following override files failed to load or apply. Check the file format and content.", + if (auto section = Util::SectionWrapper(T("menu.issues.override_failures_header", "Override Failures"), + T("menu.issues.override_failures_desc", + "The following override files failed to load or apply. Check the file format and content."), theme.StatusPalette.Error, !overrideIssues.empty())) { for (const auto* issue : overrideIssues) { DrawFeatureIssue(*issue, theme.StatusPalette.Error); @@ -418,40 +424,40 @@ namespace FeatureIssues } // Common cleanup actions section - ImGui::TextColored(theme.Palette.Text, "Cleanup Actions:"); - if (ImGui::Button("Open Features Folder")) { + ImGui::TextColored(theme.Palette.Text, "%s", T("menu.issues.cleanup_actions", "Cleanup Actions:")); + if (ImGui::Button(T("menu.issues.open_features_folder", "Open Features Folder"))) { std::filesystem::path featuresPath = Util::PathHelpers::GetFeaturesRealPath(); ShellExecuteA(NULL, "open", featuresPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens the Features folder containing INI files for manual review."); + ImGui::Text("%s", T("menu.issues.open_features_folder_tooltip", "Opens the Features folder containing INI files for manual review.")); } ImGui::SameLine(); - if (ImGui::Button("Open Shaders Directory")) { + if (ImGui::Button(T("menu.issues.open_shaders_directory", "Open Shaders Directory"))) { std::filesystem::path shadersPath = Util::PathHelpers::GetShadersRealPath(); ShellExecuteA(NULL, "open", shadersPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens the main Shaders directory to view individual feature shader folders."); + ImGui::Text("%s", T("menu.issues.open_shaders_tooltip", "Opens the main Shaders directory to view individual feature shader folders.")); } std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); if (!logPath.empty()) { ImGui::SameLine(); - if (ImGui::Button("Open Logs")) { + if (ImGui::Button(T("menu.issues.open_logs", "Open Logs"))) { ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens the CommunityShaders.log file for manual review."); + ImGui::Text("%s", T("menu.issues.open_logs_tooltip", "Opens the CommunityShaders.log file for manual review.")); } } ImGui::SameLine(); - if (ImGui::Button("Clear Issue List")) { + if (ImGui::Button(T("menu.issues.clear_issue_list", "Clear Issue List"))) { ClearFeatureIssues(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Clears this issue list (useful after cleanup)."); + ImGui::Text("%s", T("menu.issues.clear_issue_list_tooltip", "Clears this issue list (useful after cleanup).")); } ImGui::Spacing(); @@ -459,11 +465,11 @@ namespace FeatureIssues ImGui::Spacing(); // Cleanup guidance - ImGui::TextColored(theme.Palette.Text, "General Actions:"); - ImGui::BulletText("Use 'Open Features Folder' to manually review INI files"); - ImGui::BulletText("Use 'Open Shaders Directory' to check for orphaned shader folders"); - ImGui::BulletText("Use 'Open Logs' to manually review the logs"); - ImGui::BulletText("Use 'Clear Issue List' to refresh after manual cleanup"); + ImGui::TextColored(theme.Palette.Text, "%s", T("menu.issues.general_actions", "General Actions:")); + ImGui::BulletText("%s", T("menu.issues.use_open_features_folder", "Use 'Open Features Folder' to manually review INI files")); + ImGui::BulletText("%s", T("menu.issues.use_open_shaders_directory", "Use 'Open Shaders Directory' to check for orphaned shader folders")); + ImGui::BulletText("%s", T("menu.issues.use_open_logs", "Use 'Open Logs' to manually review the logs")); + ImGui::BulletText("%s", T("menu.issues.use_clear_issue_list", "Use 'Clear Issue List' to refresh after manual cleanup")); } static void DrawFeatureIssue(const FeatureIssueInfo& issue, const ImVec4& color) @@ -478,17 +484,20 @@ namespace FeatureIssues ImGui::Bullet(); ImGui::SameLine(); ImGui::TextColored(color, "%s", - issue.displayName.empty() ? issue.shortName.c_str() : issue.displayName.c_str()); + T(("menu.issues.feature_name." + issue.shortName).c_str(), + issue.displayName.empty() ? issue.shortName.c_str() : issue.displayName.c_str())); // Show detailed information in tooltip if (auto _tt = Util::HoverTooltipWrapper()) { // Show compilation failure warning at the top in red if applicable if ((issue.IsObsolete() && issue.ModifiedShaderDirectory()) || issue.IsUnknown()) { - ImGui::TextColored(color, "POTENTIAL COMPILATION FAILURE"); + ImGui::TextColored(color, "%s", T("menu.issues.potential_compilation_failure", "POTENTIAL COMPILATION FAILURE")); if (issue.IsUnknown()) { - ImGui::TextWrapped("This unknown feature may have modified core shader files and could be causing compilation failures. Unknown features should be removed if failures continue."); + ImGui::TextWrapped("%s", T("menu.issues.unknown_compilation_warning", + "This unknown feature may have modified core shader files and could be causing compilation failures. Unknown features should be removed if failures continue.")); } else { - ImGui::TextWrapped("This obsolete feature modified core shader files and is causing compilation failures. It must be uninstalled via mod manager."); + ImGui::TextWrapped("%s", T("menu.issues.obsolete_compilation_failure", + "This obsolete feature modified core shader files and is causing compilation failures. It must be uninstalled via mod manager.")); } ImGui::Spacing(); ImGui::Separator(); @@ -496,52 +505,52 @@ namespace FeatureIssues } if (!issue.iniPath.empty()) { - ImGui::TextWrapped("INI Path: %s", issue.iniPath.c_str()); + ImGui::TextWrapped(T("menu.issues.ini_path", "INI Path: %s"), issue.iniPath.c_str()); ImGui::Spacing(); } if (!issue.version.empty()) { - ImGui::TextWrapped("Current Version: %s", issue.version.c_str()); + ImGui::TextWrapped(T("menu.issues.current_version", "Current Version: %s"), issue.version.c_str()); ImGui::Spacing(); } if (issue.IsVersionMismatch() && !issue.minimumVersionRequired.empty()) { - ImGui::TextWrapped("Minimum Required: %s", issue.minimumVersionRequired.c_str()); + ImGui::TextWrapped(T("menu.issues.minimum_required", "Minimum Required: %s"), issue.minimumVersionRequired.c_str()); ImGui::Spacing(); } - ImGui::TextWrapped("Issue: %s", issue.rejectionReason.c_str()); + ImGui::TextWrapped(T("menu.issues.issue_label", "Issue: %s"), issue.rejectionReason.c_str()); if (issue.IsObsolete() && !issue.replacementFeature.empty()) { ImGui::Spacing(); - ImGui::TextWrapped("Replacement: %s", issue.replacementFeatureDisplayName.c_str()); + ImGui::TextWrapped(T("menu.issues.replacement_label", "Replacement: %s"), issue.replacementFeatureDisplayName.c_str()); } if (issue.IsObsolete() && !issue.userMessage.empty()) { ImGui::Spacing(); - ImGui::TextWrapped("Guidance: %s", issue.userMessage.c_str()); + ImGui::TextWrapped(T("menu.issues.guidance_label", "Guidance: %s"), issue.userMessage.c_str()); } // Show file information if (issue.fileInfo.hasINI || issue.fileInfo.hasDeployedFolder) { ImGui::Spacing(); ImGui::Separator(); - ImGui::TextColored(theme.Palette.Text, "Files:"); + ImGui::TextColored(theme.Palette.Text, "%s", T("menu.issues.files_label", "Files:")); if (issue.fileInfo.hasINI) { - ImGui::TextWrapped("INI: %s", issue.fileInfo.iniPath.c_str()); + ImGui::TextWrapped(T("menu.issues.ini_label", "INI: %s"), issue.fileInfo.iniPath.c_str()); } if (issue.fileInfo.hasDeployedFolder) { - ImGui::TextWrapped("Shader Folder: %s", issue.fileInfo.deployedFolderPath.c_str()); + ImGui::TextWrapped(T("menu.issues.shader_folder", "Shader Folder: %s"), issue.fileInfo.deployedFolderPath.c_str()); if (!issue.fileInfo.hlslFiles.empty()) { - ImGui::TextWrapped("HLSL Files: %zu found", issue.fileInfo.hlslFiles.size()); + ImGui::TextWrapped(T("menu.issues.hlsl_files_found", "HLSL Files: %zu found"), issue.fileInfo.hlslFiles.size()); } } // Show timestamp information if (!issue.fileInfo.timestampDisplay.empty()) { ImGui::Spacing(); - ImGui::TextColored(theme.Palette.Text, "Last Modified:"); - ImGui::TextWrapped("Time: %s", issue.fileInfo.timestampDisplay.c_str()); + ImGui::TextColored(theme.Palette.Text, "%s", T("menu.issues.last_modified", "Last Modified:")); + ImGui::TextWrapped(T("menu.issues.time_label", "Time: %s"), issue.fileInfo.timestampDisplay.c_str()); if (!issue.fileInfo.latestTimestampFile.empty()) { - ImGui::TextWrapped("File: %s", issue.fileInfo.latestTimestampFile.c_str()); + ImGui::TextWrapped(T("menu.issues.file_label", "File: %s"), issue.fileInfo.latestTimestampFile.c_str()); } } } @@ -551,36 +560,52 @@ namespace FeatureIssues if (issue.IsObsolete() && !issue.replacementFeature.empty()) { // Show replacement info using friendly name with emphasis ImGui::SameLine(); - ImGui::Text("(replaced by "); + ImGui::Text("%s", T("menu.issues.replaced_by_prefix", "(replaced by ")); ImGui::SameLine(0, 0); // No spacing ImGui::TextColored(theme.StatusPalette.RestartNeeded, "%s", issue.replacementFeatureDisplayName.c_str()); ImGui::SameLine(0, 0); // No spacing - ImGui::Text(")"); + ImGui::Text("%s", T("menu.issues.replaced_by_suffix", ")")); if (issue.replacementFeatureInstalled) { // Show "Open" button to navigate to the replacement feature ImGui::SameLine(); - if (ImGui::SmallButton(("Open " + issue.replacementFeatureDisplayName + " Settings").c_str())) { - // Navigate to the replacement feature in the menu - menu->SelectFeatureMenu(issue.replacementFeature); - logger::debug("User requested to open {} feature menu", issue.replacementFeature); + { + auto openKey = "menu.issues.open_feature_settings"; + auto openDefault = "Open " + issue.replacementFeatureDisplayName + " Settings"; + auto openText = I18n::GetSingleton()->Format(openKey, { { "name", issue.replacementFeatureDisplayName } }, openDefault.c_str()); + if (ImGui::SmallButton(openText.c_str())) { + // Navigate to the replacement feature in the menu + menu->SelectFeatureMenu(issue.replacementFeature); + logger::debug("User requested to open {} feature menu", issue.replacementFeature); + } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Open the installed %s feature settings", issue.replacementFeatureDisplayName.c_str()); + auto tipKey = "menu.issues.open_settings_tooltip"; + auto tipDefault = "Open the installed " + issue.replacementFeatureDisplayName + " feature settings"; + auto tipText = I18n::GetSingleton()->Format(tipKey, { { "name", issue.replacementFeatureDisplayName } }, tipDefault.c_str()); + ImGui::Text("%s", tipText.c_str()); } } else { // Check if replacement feature has a download link (cached) if (!issue.replacementFeatureModLink.empty()) { ImGui::SameLine(); - if (ImGui::SmallButton(("Download " + issue.replacementFeatureDisplayName).c_str())) { - ShellExecuteA(0, 0, issue.replacementFeatureModLink.c_str(), 0, 0, SW_SHOW); + { + auto dlKey = "menu.issues.download_button"; + auto dlDefault = "Download " + issue.replacementFeatureDisplayName; + auto dlText = I18n::GetSingleton()->Format(dlKey, { { "name", issue.replacementFeatureDisplayName } }, dlDefault.c_str()); + if (ImGui::SmallButton(dlText.c_str())) { + ShellExecuteA(0, 0, issue.replacementFeatureModLink.c_str(), 0, 0, SW_SHOW); + } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Download the replacement feature: %s", issue.replacementFeatureDisplayName.c_str()); + auto tipKey = "menu.issues.download_replacement_tooltip"; + auto tipDefault = "Download the replacement feature: " + issue.replacementFeatureDisplayName; + auto tipText = I18n::GetSingleton()->Format(tipKey, { { "name", issue.replacementFeatureDisplayName } }, tipDefault.c_str()); + ImGui::Text("%s", tipText.c_str()); } } } @@ -589,17 +614,27 @@ namespace FeatureIssues // Handle download action for version mismatch features if (IsVersionMismatchForCoreFeature(issue)) { ImGui::SameLine(); - ImGui::Text("Core feature already installed"); + ImGui::Text("%s", T("menu.issues.core_feature_installed", "Core feature already installed")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextWrapped("This feature is already included as part of the core Community Shaders installation. Uninstall this feature with your mod manager."); + ImGui::TextWrapped("%s", T("menu.issues.core_feature_installed_tooltip", + "This feature is already included as part of the core Community Shaders installation. Uninstall this feature with your mod manager.")); } } else if (issue.IsVersionMismatch()) { ImGui::SameLine(); if (!issue.replacementFeatureModLink.empty()) { - std::string buttonText = issue.minimumVersionRequired.empty() ? - ("Download Latest " + issue.replacementFeatureDisplayName) : - ("Download " + issue.replacementFeatureDisplayName + " " + issue.minimumVersionRequired + "+"); + std::string buttonKey; + std::string buttonDefault; + if (issue.minimumVersionRequired.empty()) { + buttonKey = "menu.issues.download_latest_button"; + buttonDefault = "Download Latest " + issue.replacementFeatureDisplayName; + } else { + buttonKey = "menu.issues.download_version_button"; + buttonDefault = "Download " + issue.replacementFeatureDisplayName + " " + issue.minimumVersionRequired + "+"; + } + auto buttonText = I18n::GetSingleton()->Format(buttonKey, + { { "name", issue.replacementFeatureDisplayName }, { "version", issue.minimumVersionRequired } }, + buttonDefault.c_str()); if (ImGui::SmallButton(buttonText.c_str())) { ShellExecuteA(0, 0, issue.replacementFeatureModLink.c_str(), 0, 0, SW_SHOW); @@ -607,23 +642,36 @@ namespace FeatureIssues if (auto _tt = Util::HoverTooltipWrapper()) { if (!issue.minimumVersionRequired.empty()) { - ImGui::Text("Download %s version %s or later", issue.replacementFeatureDisplayName.c_str(), issue.minimumVersionRequired.c_str()); + auto tipText = I18n::GetSingleton()->Format("menu.issues.download_version_tooltip", + { { "name", issue.replacementFeatureDisplayName }, { "version", issue.minimumVersionRequired } }, + "Download {name} version {version} or later"); + ImGui::Text("%s", tipText.c_str()); } else { - ImGui::Text("Download the latest version of %s", issue.replacementFeatureDisplayName.c_str()); + auto tipText = I18n::GetSingleton()->Format("menu.issues.download_latest_tooltip", + { { "name", issue.replacementFeatureDisplayName } }, + ("Download the latest version of " + issue.replacementFeatureDisplayName).c_str()); + ImGui::Text("%s", tipText.c_str()); } } } else { // Show message when no download link is available - std::string updateText = issue.minimumVersionRequired.empty() ? - "Update Required" : - ("Update to " + issue.minimumVersionRequired + "+ Required"); - - ImGui::TextWrapped("%s", updateText.c_str()); + if (issue.minimumVersionRequired.empty()) { + ImGui::TextWrapped("%s", T("menu.issues.update_required", "Update Required")); + } else { + auto updateText = I18n::GetSingleton()->Format("menu.issues.update_to_version_required", + { { "version", issue.minimumVersionRequired } }, + "Update to {version}+ Required"); + ImGui::TextWrapped("%s", updateText.c_str()); + } if (auto _tt = Util::HoverTooltipWrapper()) { if (!issue.minimumVersionRequired.empty()) { - ImGui::Text("This feature needs to be updated to version %s or later. Check the mod page manually.", issue.minimumVersionRequired.c_str()); + auto tipText = I18n::GetSingleton()->Format("menu.issues.update_version_required_tooltip", + { { "version", issue.minimumVersionRequired } }, + ("This feature needs to be updated to version " + issue.minimumVersionRequired + " or later. Check the mod page manually.").c_str()); + ImGui::Text("%s", tipText.c_str()); } else { - ImGui::Text("This feature needs to be updated but no download link is available. Check the mod page manually."); + ImGui::Text("%s", T("menu.issues.update_no_link_tooltip", + "This feature needs to be updated but no download link is available. Check the mod page manually.")); } } } @@ -633,12 +681,20 @@ namespace FeatureIssues if (!issue.IsVersionMismatch() && !issue.IsObsolete() && !issue.replacementFeatureModLink.empty()) { ImGui::SameLine(); - if (ImGui::SmallButton(("Download " + issue.replacementFeatureDisplayName).c_str())) { - ShellExecuteA(0, 0, issue.replacementFeatureModLink.c_str(), 0, 0, SW_SHOW); + { + auto dlKey = "menu.issues.download_button"; + auto dlDefault = "Download " + issue.replacementFeatureDisplayName; + auto dlText = I18n::GetSingleton()->Format(dlKey, { { "name", issue.replacementFeatureDisplayName } }, dlDefault.c_str()); + if (ImGui::SmallButton(dlText.c_str())) { + ShellExecuteA(0, 0, issue.replacementFeatureModLink.c_str(), 0, 0, SW_SHOW); + } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Download %s", issue.replacementFeatureDisplayName.c_str()); + auto tipText = I18n::GetSingleton()->Format("menu.issues.download_tooltip", + { { "name", issue.replacementFeatureDisplayName } }, + "Download {name}"); + ImGui::Text("%s", tipText.c_str()); } } // Show delete button for: @@ -656,48 +712,52 @@ namespace FeatureIssues if (auto _tt = Util::HoverTooltipWrapper()) { if (issue.IsUnknown()) { - ImGui::Text("Delete files for this unknown feature. WARNING: If this feature modified core shaders, deletion may not fix compilation issues."); + ImGui::Text("%s", T("menu.issues.delete_unknown_tooltip", + "Delete files for this unknown feature. WARNING: If this feature modified core shaders, deletion may not fix compilation issues.")); } else { - ImGui::Text("Delete all files associated with this feature (INI, shaders, etc.)"); + ImGui::Text("%s", T("menu.issues.delete_files_tooltip", + "Delete all files associated with this feature (INI, shaders, etc.)")); } } // Confirmation popup for deletion if (auto popup = Util::CenteredPopupModal(confirmPopupId.c_str())) { - ImGui::TextWrapped("Are you sure? This will delete all files for feature '%s'?", + ImGui::TextWrapped(T("menu.issues.delete_confirm", + "Are you sure? This will delete all files for feature '%s'?"), issue.displayName.empty() ? issue.shortName.c_str() : issue.displayName.c_str()); ImGui::Spacing(); // Enhanced warning for unknown features if (issue.IsUnknown()) { - ImGui::TextColored(theme.StatusPalette.Error, "WARNING:"); - ImGui::TextWrapped("This is an UNKNOWN feature. If it modified core shader files (outside of its own folder), deleting these files alone will NOT fix shader compilation issues."); + ImGui::TextColored(theme.StatusPalette.Error, "%s", T("menu.issues.warning_label", "WARNING:")); + ImGui::TextWrapped("%s", T("menu.issues.unknown_delete_warning", + "This is an UNKNOWN feature. If it modified core shader files (outside of its own folder), deleting these files alone will NOT fix shader compilation issues.")); ImGui::Spacing(); - ImGui::TextColored(theme.StatusPalette.Warning, "If compilation issues persist after deletion:"); - ImGui::BulletText("Completely uninstall the feature via your mod manager"); - ImGui::BulletText("Check for modified files in Data/Shaders/ (not in feature subfolders)"); - ImGui::BulletText("Consider reinstalling Community Shaders if issues persist"); + ImGui::TextColored(theme.StatusPalette.Warning, "%s", T("menu.issues.compilation_persist_warning", "If compilation issues persist after deletion:")); + ImGui::BulletText("%s", T("menu.issues.uninstall_via_mod_manager", "Completely uninstall the feature via your mod manager")); + ImGui::BulletText("%s", T("menu.issues.check_modified_files", "Check for modified files in Data/Shaders/ (not in feature subfolders)")); + ImGui::BulletText("%s", T("menu.issues.reinstall_cs", "Consider reinstalling Community Shaders if issues persist")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); } - ImGui::TextColored(theme.StatusPalette.Warning, "This will delete:"); + ImGui::TextColored(theme.StatusPalette.Warning, "%s", T("menu.issues.this_will_delete", "This will delete:")); if (issue.fileInfo.hasINI) { - ImGui::BulletText("INI file: %s", issue.fileInfo.iniPath.c_str()); + ImGui::BulletText(T("menu.issues.ini_file_label", "INI file: %s"), issue.fileInfo.iniPath.c_str()); } if (issue.fileInfo.hasDeployedFolder) { - ImGui::BulletText("Shader directory: %s", issue.fileInfo.deployedFolderPath.c_str()); + ImGui::BulletText(T("menu.issues.shader_directory_label", "Shader directory: %s"), issue.fileInfo.deployedFolderPath.c_str()); if (!issue.fileInfo.hlslFiles.empty()) { - ImGui::BulletText("%zu HLSL files", issue.fileInfo.hlslFiles.size()); + ImGui::BulletText(T("menu.issues.hlsl_files_count", "%zu HLSL files"), issue.fileInfo.hlslFiles.size()); } } ImGui::Spacing(); - ImGui::TextColored(theme.StatusPalette.Error, "This action cannot be undone!"); + ImGui::TextColored(theme.StatusPalette.Error, "%s", T("menu.issues.cannot_be_undone", "This action cannot be undone!")); ImGui::Spacing(); - if (ImGui::Button("Delete", ImVec2(120, 0))) { + if (ImGui::Button(T("menu.issues.delete", "Delete"), ImVec2(120, 0))) { if (DeleteFeatureFiles(issue)) { // Remove from issues list after successful deletion auto& issues = const_cast&>(GetFeatureIssues()); @@ -709,7 +769,7 @@ namespace FeatureIssues } ImGui::SetItemDefaultFocus(); ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) { + if (ImGui::Button(T("menu.issues.cancel", "Cancel"), ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } } @@ -928,10 +988,12 @@ namespace FeatureIssues LoadPersistentTestState(); if (s_activeTestInis.empty()) { - return "No test INI files are currently active."; + return T("menu.issues.test.no_active_inis", "No test INI files are currently active."); } - std::string description = std::format("Active test INI files ({}):\n", s_activeTestInis.size()); + std::string description = I18n::GetSingleton()->Format("menu.issues.test.active_inis_count", + { { "count", std::to_string(s_activeTestInis.size()) } }, + "Active test INI files ({count}):\n"); int activeCount = 0, deletedCount = 0, obsoleteCount = 0, unknownCount = 0, versionCount = 0; std::vector obsoleteFeatures, unknownFeatures, versionFeatures, deletedFeatures; @@ -981,23 +1043,31 @@ namespace FeatureIssues // Detailed breakdown by type if (obsoleteCount > 0) { - description += std::format("Obsolete features ({}): {}\n", obsoleteCount, joinWithCommas(obsoleteFeatures)); + description += I18n::GetSingleton()->Format("menu.issues.test.obsolete_breakdown", + { { "count", std::to_string(obsoleteCount) }, { "list", joinWithCommas(obsoleteFeatures) } }, + std::format("Obsolete features ({}): {}\n", obsoleteCount, joinWithCommas(obsoleteFeatures)).c_str()); } if (unknownCount > 0) { - description += std::format("Unknown features ({}): {}\n", unknownCount, joinWithCommas(unknownFeatures)); + description += I18n::GetSingleton()->Format("menu.issues.test.unknown_breakdown", + { { "count", std::to_string(unknownCount) }, { "list", joinWithCommas(unknownFeatures) } }, + std::format("Unknown features ({}): {}\n", unknownCount, joinWithCommas(unknownFeatures)).c_str()); } if (versionCount > 0) { - description += std::format("Version mismatch ({}): {}\n", versionCount, joinWithCommas(versionFeatures)); + description += I18n::GetSingleton()->Format("menu.issues.test.version_breakdown", + { { "count", std::to_string(versionCount) }, { "list", joinWithCommas(versionFeatures) } }, + std::format("Version mismatch ({}): {}\n", versionCount, joinWithCommas(versionFeatures)).c_str()); } if (deletedCount > 0) { - description += std::format("\n {} test file(s) manually deleted - markers remain for cleanup", deletedCount); + description += I18n::GetSingleton()->Format("menu.issues.test.deleted_notice", + { { "count", std::to_string(deletedCount) } }, + std::format("\n {} test file(s) manually deleted - markers remain for cleanup", deletedCount).c_str()); } - if (activeCount < s_activeTestInis.size()) { - description += "\nSome test files modified - restore recommended to clean up"; + if (activeCount < static_cast(s_activeTestInis.size())) { + description += T("menu.issues.test.modified_notice", "\nSome test files modified - restore recommended to clean up"); } return description; @@ -1323,6 +1393,7 @@ namespace FeatureIssues if (outFile.fail()) { throw std::runtime_error("Failed to write file contents"); } + TestIniInfo testInfo; testInfo.testIniPath = iniPath.string(); testInfo.isNewFile = true; @@ -1462,21 +1533,24 @@ namespace FeatureIssues auto* menu = Menu::GetSingleton(); const auto& themeSettings = menu->GetTheme(); - if (ImGui::CollapsingHeader("Testing", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (ImGui::CollapsingHeader(T("menu.issues.test.testing_header", "Testing"), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { { - auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", - "These tools create test INI files to trigger all known feature issue types for testing purposes.", + auto sectionWrapper = Util::SectionWrapper( + T("menu.issues.test.feature_issue_testing", "Feature Issue Testing"), + T("menu.issues.test.feature_issue_testing_desc", + "These tools create test INI files to trigger all known feature issue types for testing purposes."), themeSettings.Palette.Text); if (sectionWrapper) { const bool hasActiveTests = HasActiveTestInis(); if (hasActiveTests) { // Warning section using theme colors ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped("Test INI files are currently active. Restart CS to see feature issues."); + ImGui::TextWrapped("%s", T("menu.issues.test.active_inis_warning", + "Test INI files are currently active. Restart CS to see feature issues.")); ImGui::PopStyleColor(); // Show detailed test state information ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped(GetTestStateDescription().c_str()); + ImGui::TextWrapped("%s", GetTestStateDescription().c_str()); ImGui::PopStyleColor(); ImGui::Spacing(); } @@ -1489,19 +1563,19 @@ namespace FeatureIssues themeSettings.StatusPalette.RestartNeeded, themeSettings.StatusPalette.CurrentHotkey); - if (ImGui::Button("Create Test Inis", { -1, 0 })) { + if (ImGui::Button(T("menu.issues.test.create_test_inis", "Create Test Inis"), { -1, 0 })) { auto testInis = CreateTestInis(); logger::info("Created {} test INI files for feature issue testing", testInis.size()); } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Creates test INI files that trigger all known feature issue cases:\n" - "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" - "- Unknown features (fake non-existent features)\n" - "- Version mismatch (modifies existing feature version)\n" - "Restart CS after creating to see the issues in action."); + ImGui::Text("%s", T("menu.issues.test.create_test_inis_tooltip", + "Creates test INI files that trigger all known feature issue cases:\n" + "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" + "- Unknown features (fake non-existent features)\n" + "- Version mismatch (modifies existing feature version)\n" + "Restart CS after creating to see the issues in action.")); } // Restore button @@ -1512,7 +1586,7 @@ namespace FeatureIssues themeSettings.StatusPalette.Error, themeSettings.StatusPalette.CurrentHotkey); - if (ImGui::Button("Restore", { -1, 0 })) { + if (ImGui::Button(T("menu.issues.test.restore", "Restore"), { -1, 0 })) { auto& testInis = GetCurrentTestInis(); if (RestoreOriginalState(testInis)) { logger::info("Successfully restored original state"); @@ -1523,10 +1597,10 @@ namespace FeatureIssues } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Removes all test INI files and restores any modified INI files to their original state.\n" - "This undoes all changes made by 'Create Test Inis'.\n" - "Restart CS after restoring to see normal operation."); + ImGui::Text("%s", T("menu.issues.test.restore_tooltip", + "Removes all test INI files and restores any modified INI files to their original state.\n" + "This undoes all changes made by 'Create Test Inis'.\n" + "Restart CS after restoring to see normal operation.")); } } } diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index 7ff0c15f2f..f78b53119a 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -1,4 +1,7 @@ #include "CSEditor.h" +#include "I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.cs_editor." #include "Deferred.h" #include "Feature.h" @@ -12,6 +15,7 @@ #include "CSEditor/EditorWindow.h" #include #include +#include #include namespace @@ -159,7 +163,7 @@ void CSEditor::DrawSettings() EnsureWeatherListLoaded(); bool canOpen = EditorWindow::CanBeOpen(); ImGui::BeginDisabled(!canOpen); - if (ImGui::Button("Open CS Editor", { -1, 0 })) + if (ImGui::Button(T(TKEY("open_editor"), "Open CS Editor"), { -1, 0 })) OpenEditorWindow(); ImGui::EndDisabled(); @@ -196,19 +200,20 @@ void CSEditor::Prepass() void CSEditor::DrawWeatherPickerSection() { ImGui::Spacing(); - Util::DrawSectionHeader("Weather Details"); + Util::DrawSectionHeader(T(TKEY("weather_details"), "Weather Details")); const auto& themeSettings = Menu::GetSingleton()->GetTheme(); const auto& menuSettings = Menu::GetSingleton()->GetSettings(); // Show as Overlay checkbox bool showInOverlay = WeatherDetailsWindow.ShowInOverlay; - if (ImGui::Checkbox("Show in Overlay", &showInOverlay)) { + if (ImGui::Checkbox(T(TKEY("show_in_overlay"), "Show in Overlay"), &showInOverlay)) { WeatherDetailsWindow.ShowInOverlay = showInOverlay; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens weather details in a separate window that stays open\neven when the main menu is closed. "); - ImGui::Text("Toggle with "); + ImGui::Text("%s", T(TKEY("show_in_overlay_tooltip"), + "Opens weather details in a separate window that stays open\neven when the main menu is closed. ")); + ImGui::Text(T(TKEY("toggle_with"), "Toggle with ")); ImGui::SameLine(); ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.OverlayToggleKey).c_str()); } @@ -297,7 +302,7 @@ void CSEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeathe void CSEditor::DrawTimeControls() { ImGui::Spacing(); - Util::DrawSectionHeader("Time Controls"); + Util::DrawSectionHeader(T(TKEY("time_controls"), "Time Controls")); ImGui::Spacing(); EditorWindow::GetSingleton()->DrawTimeControls(); ImGui::Spacing(); @@ -306,7 +311,7 @@ void CSEditor::DrawTimeControls() void CSEditor::DrawWeatherStatusPanel() { ImGui::Spacing(); - Util::DrawSectionHeader("Weather Status"); + Util::DrawSectionHeader(T(TKEY("weather_status"), "Weather Status")); ImGui::Spacing(); auto weatherManager = WeatherManager::GetSingleton(); @@ -316,25 +321,25 @@ void CSEditor::DrawWeatherStatusPanel() if (currentWeathers.currentWeather) { // Show if weather has custom settings if (weatherManager->HasWeatherSettings(currentWeathers.currentWeather)) { - ImGui::TextColored(theme.StatusPalette.SuccessColor, "Has Custom Settings"); + ImGui::TextColored(theme.StatusPalette.SuccessColor, "%s", T(TKEY("has_custom_settings"), "Has Custom Settings")); } else { - ImGui::TextColored(theme.StatusPalette.Disable, "Using Default Settings"); + ImGui::TextColored(theme.StatusPalette.Disable, "%s", T(TKEY("using_default_settings"), "Using Default Settings")); } // Show what the current weather is - ImGui::Text("Current Weather: %s", + ImGui::Text(T(TKEY("current_weather"), "Current Weather: %s"), currentWeathers.currentWeather->GetFormEditorID() ? currentWeathers.currentWeather->GetFormEditorID() : std::format("{:08X}", currentWeathers.currentWeather->GetFormID()).c_str()); // Always reserve space for transition info to prevent UI shifting if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { - ImGui::Text("Transitioning From: %s", + ImGui::Text(T(TKEY("transitioning_from"), "Transitioning From: %s"), currentWeathers.lastWeather->GetFormEditorID() ? currentWeathers.lastWeather->GetFormEditorID() : std::format("{:08X}", currentWeathers.lastWeather->GetFormID()).c_str()); } else { - ImGui::Text("Transitioning From: No Transition"); + ImGui::Text("%s", T(TKEY("no_transition"), "Transitioning From: No Transition")); } // Always show progress bar @@ -346,17 +351,19 @@ void CSEditor::DrawWeatherStatusPanel() ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); } - ImGui::ProgressBar(displayPct, ImVec2(-1, 0), - isTransitioning ? - std::format("Transition: {:.1f}%", currentWeathers.lerpFactor * 100.0f).c_str() : - ""); + std::string transitionOverlay; + if (isTransitioning) { + float transitionPct = currentWeathers.lerpFactor * 100.0f; + transitionOverlay = std::vformat(T(TKEY("transition_progress"), "Transition: {:.1f}%"), std::make_format_args(transitionPct)); + } + ImGui::ProgressBar(displayPct, ImVec2(-1, 0), transitionOverlay.c_str()); if (!isTransitioning) { ImGui::PopStyleColor(); } } else { - ImGui::TextColored(theme.StatusPalette.Warning, "No Active Weather"); + ImGui::TextColored(theme.StatusPalette.Warning, "%s", T(TKEY("no_active_weather"), "No Active Weather")); } } @@ -443,7 +450,7 @@ ImVec4 CSEditor::GetWeatherTypeColor(RE::TESWeather* weather) void CSEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct) { if (!weather) { - ImGui::BulletText("No Weather Found"); + ImGui::BulletText("%s", T(TKEY("no_weather_found"), "No Weather Found")); return; } std::string weatherText = Util::FormatWeather(weather); @@ -452,51 +459,51 @@ void CSEditor::DisplayWeatherBasicInfo(RE::TESWeather* weather, float weatherPct bool showTooltip = CSEditor::RenderMultiColorWeatherName(weather, weatherText); if (showTooltip) { ImGui::BeginTooltip(); - ImGui::Text("Name: %s", weather->GetName() ? weather->GetName() : "Unnamed"); - ImGui::Text("Editor ID: %s", weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); - ImGui::Text("Form ID: 0x%08X", weather->GetFormID()); + ImGui::Text(T(TKEY("tooltip_name"), "Name: %s"), weather->GetName() ? weather->GetName() : "Unnamed"); + ImGui::Text(T(TKEY("tooltip_editor_id_2"), "Editor ID: %s"), weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); + ImGui::Text(T(TKEY("tooltip_form_id_2"), "Form ID: 0x%08X"), weather->GetFormID()); auto flagNames = CSEditor::GetWeatherFlagNames(weather); if (!flagNames.empty()) { std::string joinedFlags = flagNames[0]; for (size_t j = 1; j < flagNames.size(); ++j) { joinedFlags += ", " + flagNames[j]; } - ImGui::Text("Flags: %s", joinedFlags.c_str()); + ImGui::Text(T(TKEY("tooltip_flags"), "Flags: %s"), joinedFlags.c_str()); } else { - ImGui::Text("Flags: None"); + ImGui::Text("%s", T(TKEY("tooltip_flags_none"), "Flags: None")); } ImGui::EndTooltip(); } if (weatherPct >= 0.0f) { - ImGui::BulletText("Weather Percentage: %.1f%%", weatherPct * 100.0f); + ImGui::BulletText(T(TKEY("weather_percentage"), "Weather Percentage: %.1f%%"), weatherPct * 100.0f); } } void CSEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) { if (!weather || !weather->precipitationData) { - ImGui::BulletText("Particle Density: No precipitation data"); + ImGui::BulletText("%s", T(TKEY("no_precipitation_data"), "Particle Density: No precipitation data")); return; } auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; - ImGui::BulletText("Particle Density: %.3f", particleDensity); + ImGui::BulletText(T(TKEY("particle_density"), "Particle Density: %.3f"), particleDensity); GET_INSTANCE_MEMBER(particleTexture, weather->precipitationData) if (!particleTexture.textureName.empty()) { - ImGui::BulletText("Particle Texture: %s", particleTexture.textureName.c_str()); + ImGui::BulletText(T(TKEY("particle_texture"), "Particle Texture: %s"), particleTexture.textureName.c_str()); } else { - ImGui::BulletText("Particle Texture: None"); + ImGui::BulletText("%s", T(TKEY("particle_texture_none"), "Particle Texture: None")); } uint8_t precipBeginFadeIn = weather->data.precipitationBeginFadeIn; uint8_t precipEndFadeOut = weather->data.precipitationEndFadeOut; float precipBeginNormalized = precipBeginFadeIn / 255.0f; float precipEndNormalized = precipEndFadeOut / 255.0f; - ImGui::BulletText("Precip Begin Fade-In: %.3f (raw %u)", precipBeginNormalized, precipBeginFadeIn); - ImGui::BulletText("Precip End Fade-Out: %.3f (raw %u)", precipEndNormalized, precipEndFadeOut); + ImGui::BulletText(T(TKEY("precip_begin_fade_in"), "Precip Begin Fade-In: %.3f (raw %u)"), precipBeginNormalized, precipBeginFadeIn); + ImGui::BulletText(T(TKEY("precip_end_fade_out"), "Precip End Fade-Out: %.3f (raw %u)"), precipEndNormalized, precipEndFadeOut); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Precipitation fade transition parameters:", - "Begin Fade-In: Point where precipitation starts appearing", - "End Fade-Out: Point where precipitation fully disappears", - "Raw values: 0-255 (uint8), Normalized: 0.0-1.0" }); + Util::DrawMultiLineTooltip({ T(TKEY("precip_fade_info_0"), "Precipitation fade transition parameters:"), + T(TKEY("precip_fade_info_1"), "Begin Fade-In: Point where precipitation starts appearing"), + T(TKEY("precip_fade_info_2"), "End Fade-Out: Point where precipitation fully disappears"), + T(TKEY("precip_fade_info_3"), "Raw values: 0-255 (uint8), Normalized: 0.0-1.0") }); } } @@ -508,7 +515,7 @@ void CSEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiv uint8_t lightningR = weather->data.lightningColor.red; uint8_t lightningG = weather->data.lightningColor.green; uint8_t lightningB = weather->data.lightningColor.blue; - ImGui::Text("Lightning Color:"); + ImGui::Text("%s", T(TKEY("lightning_color"), "Lightning Color:")); ImGui::SameLine(); float lightningColor[3] = { lightningR / 255.0f, lightningG / 255.0f, lightningB / 255.0f }; ImGuiColorEditFlags flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel; @@ -526,31 +533,31 @@ void CSEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInteractiv weather->data.lightningColor.blue = static_cast(lightningColor[2] * 255.0f + 0.5f); } uint8_t thunderFreqRaw = (uint8_t)weather->data.thunderLightningFrequency; - ImGui::BulletText("Thunder Frequency: %u", static_cast(thunderFreqRaw)); + ImGui::BulletText(T(TKEY("thunder_frequency"), "Thunder Frequency: %u"), static_cast(thunderFreqRaw)); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Thunder frequency raw value (0-255):", + Util::DrawMultiLineTooltip({ T(TKEY("thunder_freq_info_0"), "Thunder frequency raw value (0-255):"), "", - "Known data points from Creation Kit slider:", - "- Raw 15 = ~100% frequency (highest thunder)", - "- Raw 76 = ~75% frequency", - "- Raw 203 = ~20% frequency", - "- Raw 246 = ~5% frequency", - "- Raw 255 = ~0% frequency (lowest thunder)", + T(TKEY("thunder_freq_info_1"), "Known data points from Creation Kit slider:"), + T(TKEY("thunder_freq_info_2"), "- Raw 15 = ~100% frequency (highest thunder)"), + T(TKEY("thunder_freq_info_3"), "- Raw 76 = ~75% frequency"), + T(TKEY("thunder_freq_info_4"), "- Raw 203 = ~20% frequency"), + T(TKEY("thunder_freq_info_5"), "- Raw 246 = ~5% frequency"), + T(TKEY("thunder_freq_info_6"), "- Raw 255 = ~0% frequency (lowest thunder)"), "", - "Range: 0-255 (unsigned 8-bit integer)", - "Note: Creation Kit interprets this value non-linearly" }); + T(TKEY("thunder_freq_info_7"), "Range: 0-255 (unsigned 8-bit integer)"), + T(TKEY("thunder_freq_info_8"), "Note: Creation Kit interprets this value non-linearly") }); } uint8_t lightningBeginFadeIn = weather->data.thunderLightningBeginFadeIn; uint8_t lightningEndFadeOut = weather->data.thunderLightningEndFadeOut; float lightningBeginNormalized = lightningBeginFadeIn / 255.0f; float lightningEndNormalized = lightningEndFadeOut / 255.0f; - ImGui::BulletText("Lightning Begin Fade-In: %.3f (raw %u)", lightningBeginNormalized, lightningBeginFadeIn); - ImGui::BulletText("Lightning End Fade-Out: %.3f (raw %u)", lightningEndNormalized, lightningEndFadeOut); + ImGui::BulletText(T(TKEY("lightning_begin_fade_in"), "Lightning Begin Fade-In: %.3f (raw %u)"), lightningBeginNormalized, lightningBeginFadeIn); + ImGui::BulletText(T(TKEY("lightning_end_fade_out"), "Lightning End Fade-Out: %.3f (raw %u)"), lightningEndNormalized, lightningEndFadeOut); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Lightning fade transition parameters:", - "Begin Fade-In: Point where lightning starts appearing", - "End Fade-Out: Point where lightning fully disappears", - "Raw values: 0-255 (uint8), Normalized: 0.0-1.0" }); + Util::DrawMultiLineTooltip({ T(TKEY("lightning_fade_info_0"), "Lightning fade transition parameters:"), + T(TKEY("lightning_fade_info_1"), "Begin Fade-In: Point where lightning starts appearing"), + T(TKEY("lightning_fade_info_2"), "End Fade-Out: Point where lightning fully disappears"), + T(TKEY("lightning_fade_info_3"), "Raw values: 0-255 (uint8), Normalized: 0.0-1.0") }); } } @@ -561,55 +568,59 @@ void CSEditor::DisplayWindInfo(RE::TESWeather* weather) return; const auto& theme = Menu::GetSingleton()->GetTheme(); float windSpeedDisplay = weather->data.windSpeed / 255.0f; - ImGui::BulletText("Weather Wind Speed: %.2f (raw %d)", windSpeedDisplay, weather->data.windSpeed); + ImGui::BulletText(T(TKEY("weather_wind_speed"), "Weather Wind Speed: %.2f (raw %d)"), windSpeedDisplay, weather->data.windSpeed); if (auto _tt = Util::HoverTooltipWrapper()) { std::string windStr = Util::Units::FormatWindSpeed(weather->data.windSpeed); - Util::DrawMultiLineTooltip({ "Wind speed from weather definition", + Util::DrawMultiLineTooltip({ T(TKEY("wind_speed_tooltip_0"), "Wind speed from weather definition"), windStr.c_str() }); } if (sky) { - ImGui::BulletText("Sky Wind Speed: %.2f", sky->windSpeed); + ImGui::BulletText(T(TKEY("sky_wind_speed"), "Sky Wind Speed: %.2f"), sky->windSpeed); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Current active wind speed from the sky system", - "This affects particle behavior and wind-based effects" }); + Util::DrawMultiLineTooltip({ T(TKEY("sky_wind_tooltip_0"), "Current active wind speed from the sky system"), + T(TKEY("sky_wind_tooltip_1"), "This affects particle behavior and wind-based effects") }); } } float weatherWindDirDegrees = Util::Units::DirectionRawToDegrees(weather->data.windDirection); - ImGui::BulletText("Wind Direction: %.1f° (raw %d)", weatherWindDirDegrees, weather->data.windDirection); + ImGui::BulletText(T(TKEY("wind_direction"), "Wind Direction: %.1f\xc2\xb0 (raw %d)"), weatherWindDirDegrees, weather->data.windDirection); if (auto _tt = Util::HoverTooltipWrapper()) { std::string dirStr = Util::Units::FormatDirection(weather->data.windDirection); - Util::DrawMultiLineTooltip({ "Wind direction from weather definition", + Util::DrawMultiLineTooltip({ T(TKEY("wind_direction_tooltip_0"), "Wind direction from weather definition"), dirStr.c_str() }); } float weatherWindRangeDegrees = Util::Units::DirectionRangeToDegrees(weather->data.windDirectionRange); - ImGui::BulletText("Wind Direction Range: %.1f° (raw %d)", weatherWindRangeDegrees, weather->data.windDirectionRange); + ImGui::BulletText(T(TKEY("wind_direction_range"), "Wind Direction Range: %.1f\xc2\xb0 (raw %d)"), weatherWindRangeDegrees, weather->data.windDirectionRange); if (auto player = RE::PlayerCharacter::GetSingleton()) { float playerAngleZ = player->GetAngleZ(); float playerAngleDegrees = Util::Units::NormalizeDegrees0To360(Util::Units::RadiansToDegrees(playerAngleZ)); - ImGui::BulletText("Player Direction: %.1f°", playerAngleDegrees); + ImGui::BulletText(T(TKEY("player_direction"), "Player Direction: %.1f\xc2\xb0"), playerAngleDegrees); float effectiveWindDirection = Util::Units::NormalizeDegrees0To360(weatherWindDirDegrees - WIND_DIRECTION_OFFSET); float rawDifference = Util::Units::NormalizeDegreesToSignedRange(effectiveWindDirection - playerAngleDegrees); - ImGui::BulletText("Effective Wind Dir: %.1f° (raw - %.1f°)", effectiveWindDirection, WIND_DIRECTION_OFFSET); - ImGui::BulletText("Wind vs Player: %.1f°", rawDifference); + ImGui::BulletText(T(TKEY("effective_wind_dir"), "Effective Wind Dir: %.1f\xc2\xb0 (raw - %.1f\xc2\xb0)"), effectiveWindDirection, WIND_DIRECTION_OFFSET); + ImGui::BulletText(T(TKEY("wind_vs_player"), "Wind vs Player: %.1f\xc2\xb0"), rawDifference); const char* windRelation; if (std::abs(rawDifference) < 30.0f) { - windRelation = "Tailwind (wind behind player)"; + windRelation = T(TKEY("tailwind"), "Tailwind (wind behind player)"); } else if (std::abs(rawDifference) > 150.0f) { - windRelation = "Headwind (wind coming toward player)"; + windRelation = T(TKEY("headwind"), "Headwind (wind coming toward player)"); } else if (rawDifference > 0) { - windRelation = "Right crosswind"; + windRelation = T(TKEY("right_crosswind"), "Right crosswind"); } else { - windRelation = "Left crosswind"; + windRelation = T(TKEY("left_crosswind"), "Left crosswind"); } ImGui::SameLine(); ImGui::TextColored(theme.StatusPalette.RestartNeeded, "(%s)", windRelation); if (auto _tt = Util::HoverTooltipWrapper()) { Util::DrawMultiLineTooltip({ - "Wind relative to player direction:", - "- ~0° = Tailwind (wind behind player)", - "- ~±90° = Crosswind (left/right)", - "- ~±180° = Headwind (wind coming toward player)", + T(TKEY("wind_vs_player_tooltip_0"), "Wind relative to player direction:"), + T(TKEY("wind_vs_player_tooltip_1"), "- ~0\xc2\xb0 = Tailwind (wind behind player)"), + T(TKEY("wind_vs_player_tooltip_2"), + "- ~\xc2\xb1" + "90\xc2\xb0 = Crosswind (left/right)"), + T(TKEY("wind_vs_player_tooltip_3"), + "- ~\xc2\xb1" + "180\xc2\xb0 = Headwind (wind coming toward player)"), }); } } @@ -628,17 +639,17 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) { // Weather Selection Section (only show interactive elements in inline mode) static bool weatherControlsExpanded = true; - Util::DrawSectionHeader("Weather Controls", false, true, &weatherControlsExpanded); + Util::DrawSectionHeader(T(TKEY("weather_controls"), "Weather Controls"), false, true, &weatherControlsExpanded); if (!weatherControlsExpanded) return; - ImGui::Text("Filter by Weather Type:"); - if (ImGui::Button("Select All")) { + ImGui::Text("%s", T(TKEY("filter_by_weather_type"), "Filter by Weather Type:")); + if (ImGui::Button(T(TKEY("select_all"), "Select All"))) { s_weatherFlagFilter = ALL_WEATHER_FLAGS; // All weather flags (bits 0-6, including unclassified) } ImGui::SameLine(); - if (ImGui::Button("Clear All")) { + if (ImGui::Button(T(TKEY("clear_all"), "Clear All"))) { s_weatherFlagFilter = 0x00; // No flags } // Dynamic checkbox layout - calculate how many fit per row @@ -655,13 +666,13 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) }; std::vector filters = { - { "Pleasant", RE::TESWeather::WeatherDataFlag::kPleasant, false }, - { "Cloudy", RE::TESWeather::WeatherDataFlag::kCloudy, false }, - { "Rainy", RE::TESWeather::WeatherDataFlag::kRainy, false }, - { "Snow", RE::TESWeather::WeatherDataFlag::kSnow, false }, - { "Aurora", RE::TESWeather::WeatherDataFlag::kPermAurora, false }, - { "Aurora Sun", RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun, false }, - { "None", RE::TESWeather::WeatherDataFlag::kNone, true } // Special case for unclassified + { T(TKEY("pleasant"), "Pleasant"), RE::TESWeather::WeatherDataFlag::kPleasant, false }, + { T(TKEY("cloudy"), "Cloudy"), RE::TESWeather::WeatherDataFlag::kCloudy, false }, + { T(TKEY("rainy"), "Rainy"), RE::TESWeather::WeatherDataFlag::kRainy, false }, + { T(TKEY("snow"), "Snow"), RE::TESWeather::WeatherDataFlag::kSnow, false }, + { T(TKEY("aurora"), "Aurora"), RE::TESWeather::WeatherDataFlag::kPermAurora, false }, + { T(TKEY("aurora_sun"), "Aurora Sun"), RE::TESWeather::WeatherDataFlag::kAuroraFollowsSun, false }, + { T(TKEY("none_filter"), "None"), RE::TESWeather::WeatherDataFlag::kNone, true } // Special case for unclassified }; for (size_t i = 0; i < filters.size(); ++i) { if (i > 0 && i % checkboxesPerRow != 0) { @@ -680,9 +691,9 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) // Special handling for None filter - use CheckboxFlags for consistency ImGui::CheckboxFlags(filters[i].label, &s_weatherFlagFilter, UNCLASSIFIED_FLAG); if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Shows weathers that are not classified under any specific category.", - "Includes weathers with no flags or only untracked flags.", - "Categories tracked: Pleasant, Cloudy, Rainy, Snow, Aurora, Aurora Sun" }); + Util::DrawMultiLineTooltip({ T(TKEY("none_filter_tooltip_0"), "Shows weathers that are not classified under any specific category."), + T(TKEY("none_filter_tooltip_1"), "Includes weathers with no flags or only untracked flags."), + T(TKEY("none_filter_tooltip_2"), "Categories tracked: Pleasant, Cloudy, Rainy, Snow, Aurora, Aurora Sun") }); } } else { ImGui::CheckboxFlags(filters[i].label, &s_weatherFlagFilter, static_cast(filters[i].flag)); @@ -698,11 +709,11 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) } // Accelerate checkbox - ImGui::Checkbox("Accelerate Weather Change", &s_accelerateWeatherChange); + ImGui::Checkbox(T(TKEY("accelerate_weather_change"), "Accelerate Weather Change"), &s_accelerateWeatherChange); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When enabled, weather changes instantly"); + ImGui::Text("%s", T(TKEY("accelerate_weather_change_tooltip"), "When enabled, weather changes instantly")); } // Reset Weather button - if (ImGui::Button("Reset Weather")) { + if (ImGui::Button(T(TKEY("reset_weather"), "Reset Weather"))) { sky->ResetWeather(); // Update the selection box to reflect the reset weather without double-applying s_selectedWeatherIdx = FindWeatherIndex(sky->defaultWeather); @@ -710,14 +721,14 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Resets weather to default"); + ImGui::Text("%s", T(TKEY("reset_weather_tooltip"), "Resets weather to default")); } // Lock Weather toggle ImGui::SameLine(); auto editorWindow = EditorWindow::GetSingleton(); bool isLocked = editorWindow->IsWeatherLocked(); - const char* lockLabel = isLocked ? "Unlock Weather" : "Lock Weather"; + const char* lockLabel = isLocked ? T(TKEY("unlock_weather"), "Unlock Weather") : T(TKEY("lock_weather"), "Lock Weather"); if (isLocked) { const auto& theme = Menu::GetSingleton()->GetTheme(); @@ -734,7 +745,7 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) ImGui::PopStyleColor(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text(isLocked ? "Unlock weather to allow natural changes" : "Lock current weather to prevent changes"); + ImGui::Text("%s", T(TKEY("lock_weather_tooltip"), isLocked ? "Unlock weather to allow natural changes" : "Lock current weather to prevent changes")); } // Weather Selection - now with colored text @@ -747,11 +758,11 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) // Custom combo with colored text const char* comboPreview = (s_selectedWeatherIdx >= 0 && s_selectedWeatherIdx < static_cast(weatherLabels.size())) ? weatherLabels[s_selectedWeatherIdx].c_str() : - "Select Weather"; + T(TKEY("select_weather"), "Select Weather"); static constexpr const char* kWeatherSearchId = "WeatherPicker"; - if (ImGui::BeginCombo("Weather", comboPreview)) { + if (ImGui::BeginCombo(T(TKEY("weather"), "Weather"), comboPreview)) { auto searchText = Util::DrawComboSearchInput(kWeatherSearchId); for (int i = 0; i < static_cast(s_filteredWeathers.size()); ++i) { @@ -794,9 +805,9 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::Text("Weather: %s", weather->GetName() ? weather->GetName() : "Unnamed"); - ImGui::Text("Editor ID: %s", weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); - ImGui::Text("Form ID: 0x%08X", weather->GetFormID()); + ImGui::Text(T(TKEY("tooltip_weather_name"), "Weather: %s"), weather->GetName() ? weather->GetName() : "Unnamed"); + ImGui::Text(T(TKEY("tooltip_editor_id"), "Editor ID: %s"), weather->GetFormEditorID() ? weather->GetFormEditorID() : "None"); + ImGui::Text(T(TKEY("tooltip_form_id"), "Form ID: 0x%08X"), weather->GetFormID()); ImGui::EndTooltip(); } @@ -814,7 +825,7 @@ void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiv ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing(); - Util::DrawSectionHeader("Weather Information", false, true); + Util::DrawSectionHeader(T(TKEY("weather_information"), "Weather Information"), false, true); // Update cache: store current lastWeather if it exists, otherwise keep the cached one if (sky->lastWeather) { @@ -827,8 +838,8 @@ void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiv // Create resizable 2-column table for current and last weather if (ImGui::BeginTable("WeatherComparison", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) { // Set up columns - ImGui::TableSetupColumn("Current Weather", ImGuiTableColumnFlags_WidthStretch, 0.5f); - ImGui::TableSetupColumn("Last Weather", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn(T(TKEY("current_weather_column"), "Current Weather"), ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn(T(TKEY("last_weather_column"), "Last Weather"), ImGuiTableColumnFlags_WidthStretch, 0.5f); ImGui::TableHeadersRow(); ImGui::TableNextRow(); @@ -861,10 +872,10 @@ void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements) RenderWeatherInformationDisplay(sky, showInteractiveElements); ImGui::Spacing(); } else { - showError("Sky not in full mode"); + showError(T(TKEY("sky_not_full"), "Sky not in full mode")); } } else { - showError("Sky not available"); + showError(T(TKEY("sky_not_available"), "Sky not available")); } } @@ -965,9 +976,12 @@ void CSEditor::RenderFeatureWeatherAnalysis() // Create collapsible header for feature weather analysis bool isExpanded = ImGui::CollapsingHeader(weatherConfig.sectionName.c_str(), ImGuiTreeNodeFlags_DefaultOpen); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Weather analysis provided by: %s", feature->GetName().c_str()); - ImGui::Text("Feature category: %s", std::string(feature->GetCategory()).c_str()); - ImGui::Text("Click to %s this feature's weather data", isExpanded ? "collapse" : "expand"); + ImGui::Text("%s", T(TKEY("feature_weather_analysis_tooltip_0"), "Weather analysis provided by: ")); + ImGui::Text("%s", feature->GetDisplayName().c_str()); + ImGui::Text("%s", T(TKEY("feature_weather_analysis_tooltip_1"), "Feature category: ")); + ImGui::Text("%s", feature->GetDisplayCategory().c_str()); + ImGui::Text(T(TKEY("feature_weather_analysis_tooltip_2"), "Click to %s this feature's weather data"), + isExpanded ? T(TKEY("collapse"), "collapse") : T(TKEY("expand"), "expand")); } if (isExpanded && weatherConfig.drawFunction) { @@ -997,15 +1011,13 @@ std::vector CSEditor::GetWeatherFlagNames(RE::TESWeather* weather) for (auto flagValue : magic_enum::enum_values()) { if (flagValue != RE::TESWeather::WeatherDataFlag::kNone && weather->data.flags.any(flagValue)) { - // Convert enum name to human-readable format + // Convert enum name to canonical format (strip 'k' prefix) std::string flagName = std::string(magic_enum::enum_name(flagValue)); - - // Remove 'k' prefix and convert to readable format if (flagName.starts_with("k")) { flagName = flagName.substr(1); } - // Convert specific cases to more readable names + // Use canonical English names for logic (PermAurora → Aurora, AuroraFollowsSun → Aurora Sun) if (flagName == "PermAurora") { flagName = "Aurora"; } else if (flagName == "AuroraFollowsSun") { @@ -1026,7 +1038,7 @@ std::vector CSEditor::GetWeatherFlagNames(RE::TESWeather* weather) uint32_t unknownFlags = flags & ~knownFlags; if (unknownFlags != 0) { - flagNames.push_back("Unknown(" + std::to_string(unknownFlags) + ")"); + flagNames.push_back(std::format("{}({})", T(TKEY("unknown"), "Unknown"), unknownFlags)); } return flagNames; @@ -1085,7 +1097,11 @@ bool CSEditor::RenderMultiColorWeatherName(RE::TESWeather* weather, const std::s ImGui::SameLine(); ImVec4 flagColor = GetWeatherFlagColorByName(flagNames[i]); ImGui::PushStyleColor(ImGuiCol_Text, flagColor); - ImGui::Text("[%s]", flagNames[i].c_str()); + // Translate canonical flag name for display + std::string flagKey = std::string(TKEY("flag_")) + flagNames[i]; + std::transform(flagKey.begin(), flagKey.end(), flagKey.begin(), ::tolower); + const char* displayFlag = T(flagKey.c_str(), flagNames[i].c_str()); + ImGui::Text("[%s]", displayFlag); ImGui::PopStyleColor(); } @@ -1156,6 +1172,8 @@ std::string CSEditor::GetDisplayName(const RE::TESWeather* weather) return std::to_string(weather->GetFormID()); } +#undef I18N_KEY_PREFIX + void CSEditor::DrawOverlay() { auto player = RE::PlayerCharacter::GetSingleton(); diff --git a/src/Features/CSEditor.h b/src/Features/CSEditor.h index 0948a13c6b..4db82f4694 100644 --- a/src/Features/CSEditor.h +++ b/src/Features/CSEditor.h @@ -1,6 +1,7 @@ #pragma once #include "Buffer.h" +#include "I18n/I18n.h" #include "Menu.h" #include "OverlayFeature.h" #include "State.h" @@ -15,6 +16,7 @@ struct CSEditor : OverlayFeature } virtual inline std::string GetName() override { return "CS Editor"; } + virtual std::string GetDisplayName() override { return T("feature.cs_editor.name", "CS Editor"); } virtual inline std::string GetShortName() override { return "CSEditor"; } virtual inline std::string_view GetShaderDefineName() override { return "CS_EDITOR"; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kUtility; } @@ -24,17 +26,15 @@ struct CSEditor : OverlayFeature virtual inline std::pair> GetFeatureSummary() override { - return { - "Development tool for inspecting, editing, and previewing renderer-facing data in-game.", - { "Provides weather editing functionality", - "Includes dynamic saving and loading of vanilla post processing and weather settings.", - "Real-time editing and previewing of effects", - "Instantly switch between any weather with immediate or gradual transitions", - "Filter weather by type (Pleasant, Cloudy, Rainy, Snow, Aurora) for easy browsing", - "View detailed weather information including wind, precipitation, and lightning data", - "Color-coded weather names show all weather properties at a glance", - "Persistent overlay window for continuous weather monitoring while playing" } - }; + return { T("feature.cs_editor.description", "Development tool for inspecting, editing, and previewing renderer-facing data in-game."), + { T("feature.cs_editor.key_feature_1", "Provides weather editing functionality"), + T("feature.cs_editor.key_feature_2", "Includes dynamic saving and loading of vanilla post processing and weather settings."), + T("feature.cs_editor.key_feature_3", "Real-time editing and previewing of effects"), + T("feature.cs_editor.key_feature_4", "Instantly switch between any weather with immediate or gradual transitions"), + T("feature.cs_editor.key_feature_5", "Filter weather by type (Pleasant, Cloudy, Rainy, Snow, Aurora) for easy browsing"), + T("feature.cs_editor.key_feature_6", "View detailed weather information including wind, precipitation, and lightning data"), + T("feature.cs_editor.key_feature_7", "Color-coded weather names show all weather properties at a glance"), + T("feature.cs_editor.key_feature_8", "Persistent overlay window for continuous weather monitoring while playing") } }; } virtual void DrawSettings() override; diff --git a/src/Features/CloudShadows.cpp b/src/Features/CloudShadows.cpp index 4cb8676686..fe958a0ce1 100644 --- a/src/Features/CloudShadows.cpp +++ b/src/Features/CloudShadows.cpp @@ -1,21 +1,26 @@ #include "CloudShadows.h" +#include "../I18n/I18n.h" #include "State.h" #include "Utils/D3D.h" +#define I18N_KEY_PREFIX "feature.cloud_shadows." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( CloudShadows::Settings, Opacity) void CloudShadows::DrawSettings() { - ImGui::SliderFloat("Opacity", &settings.Opacity, 0.0f, 1.0f, "%.1f"); + ImGui::SliderFloat(T(TKEY("opacity"), "Opacity"), &settings.Opacity, 0.0f, 1.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Higher values make cloud shadows darker."); + ImGui::Text("%s", T(TKEY("opacity_tooltip"), + "Higher values make cloud shadows darker.")); } } +#undef I18N_KEY_PREFIX + void CloudShadows::LoadSettings(json& o_json) { settings = o_json; diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index 3ab14cbfef..f25123a035 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -15,21 +15,21 @@ struct CloudShadows : Feature Settings settings; virtual inline std::string GetName() override { return "Cloud Shadows"; } + virtual std::string GetDisplayName() override { return T("feature.cloud_shadows.name", "Cloud Shadows"); } virtual inline std::string GetShortName() override { return "CloudShadows"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual std::string_view GetCategory() const override { return FeatureCategories::kSky; } virtual inline std::string_view GetShaderDefineName() override { return "CLOUD_SHADOWS"; } virtual std::pair> GetFeatureSummary() override { - return { - "Adds realistic cloud shadows that move across the landscape, creating dynamic lighting changes as clouds pass overhead, enhancing atmospheric immersion.", - { "Dynamic cloud shadow projection on terrain and objects", - "Configurable shadow opacity for artistic control", - "Real-time shadow movement synchronized with cloud motion", - "Cubemap-based shadow calculation for accurate projection", - "Enhanced sky rendering integration" } - }; - } + return { T("feature.cloud_shadows.description", "Adds realistic cloud shadows that move across the landscape, creating dynamic lighting changes as clouds pass overhead, enhancing atmospheric immersion."), + { T("feature.cloud_shadows.key_feature_1", "Dynamic cloud shadow projection on terrain and objects"), + T("feature.cloud_shadows.key_feature_2", "Configurable shadow opacity for artistic control"), + T("feature.cloud_shadows.key_feature_3", "Real-time shadow movement synchronized with cloud motion"), + T("feature.cloud_shadows.key_feature_4", "Cubemap-based shadow calculation for accurate projection"), + T("feature.cloud_shadows.key_feature_5", "Enhanced sky rendering integration") } }; + }; + virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } bool overrideSky = false; diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 5fe922e753..5721ee8f35 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -3,10 +3,13 @@ #include #include +#include "I18n/I18n.h" #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" +#define I18N_KEY_PREFIX "feature.dynamic_cubemaps." + constexpr auto MIPLEVELS = 8; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -26,28 +29,28 @@ std::vector> DynamicCubemaps::GetS void DynamicCubemaps::DrawSettings() { - if (ImGui::TreeNodeEx("Screen Space Reflections", ImGuiTreeNodeFlags_DefaultOpen)) { - recompileFlag |= ImGui::Checkbox("Enable Screen Space Reflections", reinterpret_cast(&settings.EnabledSSR)); + if (ImGui::TreeNodeEx(T(TKEY("screen_space_reflections"), "Screen Space Reflections"), ImGuiTreeNodeFlags_DefaultOpen)) { + recompileFlag |= ImGui::Checkbox(T(TKEY("enable_ssr"), "Enable Screen Space Reflections"), reinterpret_cast(&settings.EnabledSSR)); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable Screen Space Reflections on Water"); + ImGui::Text("%s", T(TKEY("enable_ssr_tooltip"), "Enable Screen Space Reflections on Water")); if (REL::Module::IsVR() && !enabledAtBoot) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); - ImGui::Text( - "A restart is required to enable in VR. " - "Save Settings after enabling and restart the game."); + ImGui::Text("%s", T(TKEY("vr_restart_required"), + "A restart is required to enable in VR. " + "Save Settings after enabling and restart the game.")); ImGui::PopStyleColor(); } } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Dynamic Cubemap Creator", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("You must enable creator mode by adding the shader define CREATOR"); - ImGui::Checkbox("Enable Creator", reinterpret_cast(&settings.EnabledCreator)); + if (ImGui::TreeNodeEx(T(TKEY("dynamic_cubemap_creator"), "Dynamic Cubemap Creator"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", T(TKEY("creator_info"), "You must enable creator mode by adding the shader define CREATOR")); + ImGui::Checkbox(T(TKEY("enable_creator"), "Enable Creator"), reinterpret_cast(&settings.EnabledCreator)); if (settings.EnabledCreator) { - ImGui::ColorEdit3("Color", reinterpret_cast(&settings.CubemapColor)); - ImGui::SliderFloat("Roughness", &settings.CubemapColor.w, 0.0f, 1.0f, "%.2f"); - if (ImGui::Button("Export")) { + ImGui::ColorEdit3(T(TKEY("color"), "Color"), reinterpret_cast(&settings.CubemapColor)); + ImGui::SliderFloat(T(TKEY("roughness"), "Roughness"), &settings.CubemapColor.w, 0.0f, 1.0f, "%.2f"); + if (ImGui::Button(T(TKEY("export"), "Export"))) { auto device = globals::d3d::device; auto context = globals::d3d::context; @@ -120,7 +123,7 @@ void DynamicCubemaps::DrawSettings() ImGui::TreePop(); } if (REL::Module::IsVR()) { - if (ImGui::TreeNodeEx("Advanced VR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNodeEx(T(TKEY("advanced_vr_settings"), "Advanced VR Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR"); Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR"); ImGui::TreePop(); @@ -847,3 +850,4 @@ void DynamicCubemaps::Reset() fakeReflections = true; } } +#undef I18N_KEY_PREFIX diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index 7b23744d41..e26615f6cf 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -125,20 +125,20 @@ struct DynamicCubemaps : Feature void PostDeferred(); virtual inline std::string GetName() override { return "Dynamic Cubemaps"; } + virtual std::string GetDisplayName() override { return T("feature.dynamic_cubemaps.name", "Dynamic Cubemaps"); } virtual inline std::string GetShortName() override { return "DynamicCubemaps"; } virtual inline std::string_view GetShaderDefineName() override { return "DYNAMIC_CUBEMAPS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces.", - { "Real-time environment capture for realistic reflections", - "Dynamic cube map generation based on camera position", - "Enhanced water reflections with environmental details", - "Support for both standard and VR rendering modes", - "Optimized cubemap inference and irradiance calculation" } - }; - } + return { T("feature.dynamic_cubemaps.description", "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces."), + { T("feature.dynamic_cubemaps.key_feature_1", "Real-time environment capture for realistic reflections"), + T("feature.dynamic_cubemaps.key_feature_2", "Dynamic cube map generation based on camera position"), + T("feature.dynamic_cubemaps.key_feature_3", "Enhanced water reflections with environmental details"), + T("feature.dynamic_cubemaps.key_feature_4", "Support for both standard and VR rendering modes"), + T("feature.dynamic_cubemaps.key_feature_5", "Optimized cubemap inference and irradiance calculation") } }; + }; + virtual std::vector> GetShaderDefineOptions() override; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/ExponentialHeightFog.cpp b/src/Features/ExponentialHeightFog.cpp index ddf40c2e3f..cea43ff252 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -1,7 +1,10 @@ #include "ExponentialHeightFog.h" +#include "I18n/I18n.h" #include "WeatherVariableRegistry.h" +#define I18N_KEY_PREFIX "feature.exp_height_fog." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ExponentialHeightFog::Settings, enabled, @@ -37,33 +40,33 @@ void ExponentialHeightFog::SaveSettings(json& o_json) void ExponentialHeightFog::DrawSettings() { - ImGui::Checkbox("Enable Exponential Height Fog", (bool*)&settings.enabled); - Util::WeatherUI::SliderFloat("Start Distance", this, "startDistance", &settings.startDistance, 0.0f, 100000.0f, "%.1f"); - Util::WeatherUI::SliderFloat("Fog Height", this, "fogHeight", &settings.fogHeight, -22000.0f, 22000.0f, "%.1f"); - Util::WeatherUI::SliderFloat("Fog Height Falloff", this, "fogHeightFalloff", &settings.fogHeightFalloff, 0.001f, 2.0f, "%.3f"); - Util::WeatherUI::ColorEdit4("Fog Inscattering Color", this, "fogInscatteringColor", (float*)&settings.fogInscatteringColor); - Util::WeatherUI::SliderFloat("Original Fog Color Amount", this, "originalFogColorAmount", &settings.originalFogColorAmount, 0.0f, 1.0f, "%.2f"); - Util::WeatherUI::SliderFloat("Fog Density", this, "fogDensity", &settings.fogDensity, 0.0f, 1.0f, "%.3f"); - Util::WeatherUI::SliderFloat("Directional Light Inscattering Multiplier", this, "directionalInscatteringMultiplier", &settings.directionalInscatteringMultiplier, 0.0f, 10.0f, "%.2f"); - Util::WeatherUI::SliderFloat("Sunlight Attenuation Amount", this, "sunlightAttenuationAmount", &settings.sunlightAttenuationAmount, 0.0f, 1.0f, "%.2f"); - Util::WeatherUI::SliderFloat("Directional Light Inscattering Anisotropy", this, "directionalInscatteringAnisotropy", &settings.directionalInscatteringAnisotropy, -0.99f, 0.99f, "%.3f"); + ImGui::Checkbox(T(TKEY("enable_exp_height_fog"), "Enable Exponential Height Fog"), (bool*)&settings.enabled); + Util::WeatherUI::SliderFloat(T(TKEY("start_distance"), "Start Distance"), this, "startDistance", &settings.startDistance, 0.0f, 100000.0f, "%.1f"); + Util::WeatherUI::SliderFloat(T(TKEY("fog_height"), "Fog Height"), this, "fogHeight", &settings.fogHeight, -22000.0f, 22000.0f, "%.1f"); + Util::WeatherUI::SliderFloat(T(TKEY("fog_height_falloff"), "Fog Height Falloff"), this, "fogHeightFalloff", &settings.fogHeightFalloff, 0.001f, 2.0f, "%.3f"); + Util::WeatherUI::ColorEdit4(T(TKEY("fog_inscattering_color"), "Fog Inscattering Color"), this, "fogInscatteringColor", (float*)&settings.fogInscatteringColor); + Util::WeatherUI::SliderFloat(T(TKEY("original_fog_color_amount"), "Original Fog Color Amount"), this, "originalFogColorAmount", &settings.originalFogColorAmount, 0.0f, 1.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("fog_density"), "Fog Density"), this, "fogDensity", &settings.fogDensity, 0.0f, 1.0f, "%.3f"); + Util::WeatherUI::SliderFloat(T(TKEY("dir_inscattering_mul"), "Directional Light Inscattering Multiplier"), this, "directionalInscatteringMultiplier", &settings.directionalInscatteringMultiplier, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("sunlight_attenuation"), "Sunlight Attenuation Amount"), this, "sunlightAttenuationAmount", &settings.sunlightAttenuationAmount, 0.0f, 1.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("dir_inscattering_anisotropy"), "Directional Light Inscattering Anisotropy"), this, "directionalInscatteringAnisotropy", &settings.directionalInscatteringAnisotropy, -0.99f, 0.99f, "%.3f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Controls the asymmetry of inscattering via the Henyey-Greenstein phase function.\n" - "Positive values produce forward scattering (glow around sun).\n" - "Zero is isotropic. Negative values produce back scattering."); + ImGui::Text("%s", T(TKEY("dir_inscattering_anisotropy_tooltip"), + "Controls the asymmetry of inscattering via the Henyey-Greenstein phase function.\n" + "Positive values produce forward scattering (glow around sun).\n" + "Zero is isotropic. Negative values produce back scattering.")); } - ImGui::Checkbox("Disable Vanilla Fog", (bool*)&settings.disableVanillaFog); + ImGui::Checkbox(T(TKEY("disable_vanilla_fog"), "Disable Vanilla Fog"), (bool*)&settings.disableVanillaFog); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disables the vanilla fog entirely. Only exponential height fog will be applied."); + ImGui::Text("%s", T(TKEY("disable_vanilla_fog_tooltip"), "Disables the vanilla fog entirely. Only exponential height fog will be applied.")); } - Util::WeatherUI::Checkbox("Apply Vanilla Fade", this, "respectVanillaFogFade", (bool*)&settings.respectVanillaFogFade); + Util::WeatherUI::Checkbox(T(TKEY("apply_vanilla_fade"), "Apply Vanilla Fade"), this, "respectVanillaFogFade", (bool*)&settings.respectVanillaFogFade); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Applies vanilla fade brightness to exponential height fog."); + ImGui::Text("%s", T(TKEY("apply_vanilla_fade_tooltip"), "Applies vanilla fade brightness to exponential height fog.")); } - ImGui::Checkbox("Use Dynamic Cubemaps for Inscattering", (bool*)&settings.useDynamicCubemaps); - Util::WeatherUI::ColorEdit4("Inscattering Cubemap Tint", this, "inscatteringTint", (float*)&settings.inscatteringTint); - ImGui::SliderFloat("Cubemap Mip Level", &settings.cubemapMipLevel, 1.0f, 7.0f, "%.1f"); + ImGui::Checkbox(T(TKEY("use_dynamic_cubemaps"), "Use Dynamic Cubemaps for Inscattering"), (bool*)&settings.useDynamicCubemaps); + Util::WeatherUI::ColorEdit4(T(TKEY("inscattering_cubemap_tint"), "Inscattering Cubemap Tint"), this, "inscatteringTint", (float*)&settings.inscatteringTint); + ImGui::SliderFloat(T(TKEY("cubemap_mip_level"), "Cubemap Mip Level"), &settings.cubemapMipLevel, 1.0f, 7.0f, "%.1f"); } void ExponentialHeightFog::RegisterWeatherVariables() @@ -167,3 +170,4 @@ void ExponentialHeightFog::RegisterWeatherVariables() return factor > 0.5f ? to : from; })); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/ExponentialHeightFog.h b/src/Features/ExponentialHeightFog.h index 2d2df0314d..f9e41e99cf 100644 --- a/src/Features/ExponentialHeightFog.h +++ b/src/Features/ExponentialHeightFog.h @@ -8,21 +8,18 @@ struct ExponentialHeightFog : Feature public: virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Exponential Height Fog"; } + virtual std::string GetDisplayName() override { return T("feature.exponential_height_fog.name", "Exponential Height Fog"); } virtual inline std::string GetShortName() override { return "ExponentialHeightFog"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual inline std::pair> GetFeatureSummary() override { - return { - "Exponential Height Fog adds a realistic fog effect that increases in density with height, enhancing atmospheric depth and immersion in the game environment.", - { - "Added exponential height fog effect", - "Adapted to vanilla fog settings", - "Creates atmospheric depth", - } - }; - } + return { T("feature.exponential_height_fog.description", "Exponential Height Fog adds a realistic fog effect that increases in density with height, enhancing atmospheric depth and immersion in the game environment."), + { T("feature.exponential_height_fog.key_feature_1", "Added exponential height fog effect"), + T("feature.exponential_height_fog.key_feature_2", "Adapted to vanilla fog settings"), + T("feature.exponential_height_fog.key_feature_3", "Creates atmospheric depth") } }; + }; virtual inline std::string_view GetShaderDefineName() override { return "EXP_HEIGHT_FOG"; } bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/ExtendedMaterials.cpp b/src/Features/ExtendedMaterials.cpp index 5bab1f88f2..98df52c36d 100644 --- a/src/Features/ExtendedMaterials.cpp +++ b/src/Features/ExtendedMaterials.cpp @@ -1,4 +1,7 @@ #include "ExtendedMaterials.h" +#include "../I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.extended_materials." NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ExtendedMaterials::Settings, @@ -24,13 +27,13 @@ void ExtendedMaterials::DataLoaded() void ExtendedMaterials::DrawSettings() { - if (ImGui::TreeNodeEx("Complex Material", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Complex Material", (bool*)&settings.EnableComplexMaterial); + if (ImGui::TreeNodeEx(T(TKEY("complex_material"), "Complex Material"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_complex_material"), "Enable Complex Material"), (bool*)&settings.EnableComplexMaterial); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Enables support for the Complex Material specification which makes use of the environment mask. " - "This includes parallax, as well as more realistic metals and specular reflections. " - "May lead to some warped textures on modded content which have an invalid alpha channel in their environment mask. "); + ImGui::Text("%s", T(TKEY("enable_complex_material_tooltip"), + "Enables support for the Complex Material specification which makes use of the environment mask. " + "This includes parallax, as well as more realistic metals and specular reflections. " + "May lead to some warped textures on modded content which have an invalid alpha channel in their environment mask. ")); } ImGui::Spacing(); @@ -38,29 +41,29 @@ void ExtendedMaterials::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Parallax", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Parallax", (bool*)&settings.EnableParallax); + if (ImGui::TreeNodeEx(T(TKEY("parallax"), "Parallax"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_parallax"), "Enable Parallax"), (bool*)&settings.EnableParallax); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables parallax on standard meshes made for parallax."); + ImGui::Text("%s", T(TKEY("enable_parallax_tooltip"), "Enables parallax on standard meshes made for parallax.")); } - if (ImGui::Checkbox("Enable Legacy Terrain", (bool*)&settings.EnableTerrain)) { + if (ImGui::Checkbox(T(TKEY("enable_legacy_terrain"), "Enable Legacy Terrain"), (bool*)&settings.EnableTerrain)) { if (settings.EnableTerrain) { DataLoaded(); } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Enables terrain parallax using the alpha channel of each landscape texture. " - "Therefore, all landscape textures must support parallax for this effect to work properly. "); + ImGui::Text("%s", T(TKEY("enable_legacy_terrain_tooltip"), + "Enables terrain parallax using the alpha channel of each landscape texture. " + "Therefore, all landscape textures must support parallax for this effect to work properly. ")); } - ImGui::Checkbox("Enable Terrain Height Blending", (bool*)&settings.EnableHeightBlending); + ImGui::Checkbox(T(TKEY("enable_height_blending"), "Enable Terrain Height Blending"), (bool*)&settings.EnableHeightBlending); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables landscape texture blending based on parallax. "); + ImGui::Text("%s", T(TKEY("enable_height_blending_tooltip"), "Enables landscape texture blending based on parallax. ")); } - ImGui::Checkbox("Enable Parallax Warping Fix", (bool*)&settings.EnableParallaxWarpingFix); + ImGui::Checkbox(T(TKEY("enable_parallax_warping_fix"), "Enable Parallax Warping Fix"), (bool*)&settings.EnableParallaxWarpingFix); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables a fix reducing parallax scale on curved and smooth normal triangles."); + ImGui::Text("%s", T(TKEY("enable_parallax_warping_fix_tooltip"), "Enables a fix reducing parallax scale on curved and smooth normal triangles.")); } ImGui::Spacing(); @@ -68,17 +71,17 @@ void ExtendedMaterials::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Approximate Soft Shadows", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Shadows", (bool*)&settings.EnableShadows); + if (ImGui::TreeNodeEx(T(TKEY("soft_shadows"), "Approximate Soft Shadows"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_shadows"), "Enable Shadows"), (bool*)&settings.EnableShadows); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Enables cheap soft shadows when using parallax. " - "This applies to all directional and point lights. "); + ImGui::Text("%s", T(TKEY("enable_shadows_tooltip"), + "Enables cheap soft shadows when using parallax. " + "This applies to all directional and point lights. ")); } - ImGui::Checkbox("Extend Shadows", (bool*)&settings.ExtendShadows); + ImGui::Checkbox(T(TKEY("extend_shadows"), "Extend Shadows"), (bool*)&settings.ExtendShadows); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Extends parallax shadows beyond the range of parallax. Small performance impact."); + ImGui::Text("%s", T(TKEY("extend_shadows_tooltip"), + "Extends parallax shadows beyond the range of parallax. Small performance impact.")); } ImGui::Spacing(); @@ -87,6 +90,8 @@ void ExtendedMaterials::DrawSettings() } } +#undef I18N_KEY_PREFIX + void ExtendedMaterials::LoadSettings(json& o_json) { settings = o_json; @@ -110,4 +115,4 @@ bool ExtendedMaterials::HasShaderDefine(RE::BSShader::Type shaderType) default: return false; } -} \ No newline at end of file +} diff --git a/src/Features/ExtendedMaterials.h b/src/Features/ExtendedMaterials.h index 10519a9a4f..0f3bd19a4d 100644 --- a/src/Features/ExtendedMaterials.h +++ b/src/Features/ExtendedMaterials.h @@ -5,22 +5,20 @@ struct ExtendedMaterials : Feature { virtual inline std::string GetName() override { return "Extended Materials"; } + virtual std::string GetDisplayName() override { return T("feature.extended_materials.name", "Extended Materials"); } virtual inline std::string GetShortName() override { return "ExtendedMaterials"; } virtual inline std::string_view GetShaderDefineName() override { return "EXTENDED_MATERIALS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } virtual std::pair> GetFeatureSummary() override { - return { - "Extended Materials adds advanced material effects including parallax occlusion mapping and complex material blending.\n" - "This feature enhances surface detail and depth perception for more realistic textures.", - { "Parallax occlusion mapping for depth", - "Complex material blending", - "Terrain heightmap support", - "Parallax shadows", - "Height-based texture blending" } - }; - } + return { T("feature.extended_materials.description", "Extended Materials adds advanced material effects including parallax occlusion mapping and complex material blending.\nThis feature enhances surface detail and depth perception for more realistic textures."), + { T("feature.extended_materials.key_feature_1", "Parallax occlusion mapping for depth"), + T("feature.extended_materials.key_feature_2", "Complex material blending"), + T("feature.extended_materials.key_feature_3", "Terrain heightmap support"), + T("feature.extended_materials.key_feature_4", "Parallax shadows"), + T("feature.extended_materials.key_feature_5", "Height-based texture blending") } }; + }; bool HasShaderDefine(RE::BSShader::Type shaderType) override; diff --git a/src/Features/ExtendedTranslucency.cpp b/src/Features/ExtendedTranslucency.cpp index 7d2f92ede2..50212399f4 100644 --- a/src/Features/ExtendedTranslucency.cpp +++ b/src/Features/ExtendedTranslucency.cpp @@ -1,9 +1,12 @@ #include "ExtendedTranslucency.h" +#include "../I18n/I18n.h" #include "../ShaderCache.h" #include "../State.h" #include "../Util.h" +#define I18N_KEY_PREFIX "feature.extended_translucency." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ExtendedTranslucency::Settings, AlphaMode, @@ -84,54 +87,54 @@ void ExtendedTranslucency::PostPostLoad() void ExtendedTranslucency::DrawSettings() { - if (ImGui::TreeNodeEx("Translucent Material", ImGuiTreeNodeFlags_DefaultOpen)) { - static constexpr const char* AlphaModeNames[] = { - "0 - Disabled", - "1 - Rim Edge", - "2 - Isotropic Fabric, Glass, ...", - "3 - Anisotropic Fabric", + if (ImGui::TreeNodeEx(T(TKEY("translucent_material"), "Translucent Material"), ImGuiTreeNodeFlags_DefaultOpen)) { + const char* AlphaModeNames[] = { + T(TKEY("alpha_mode_disabled"), "0 - Disabled"), + T(TKEY("alpha_mode_rim_edge"), "1 - Rim Edge"), + T(TKEY("alpha_mode_isotropic_fabric"), "2 - Isotropic Fabric, Glass, ..."), + T(TKEY("alpha_mode_anisotropic_fabric"), "3 - Anisotropic Fabric"), }; static constexpr int AlphaModeSize = static_cast(std::size(AlphaModeNames)); bool changed = false; - if (ImGui::Combo("Default Material Model", (int*)&settings.AlphaMode, AlphaModeNames, AlphaModeSize)) { + if (ImGui::Combo(T(TKEY("default_material_model"), "Default Material Model"), (int*)&settings.AlphaMode, AlphaModeNames, AlphaModeSize)) { changed = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Anisotropic transluency will adjust the opacity based on your view angle to the translucent surface.\n" - " - Disabled: No anisotropic transluency, flat alpha.\n" - " - Rim Edge: Naive rim light effect with no physics model, the edge of the geometry is always opaque even its full transparent.\n" - " - Isotropic Fabric: Imaginary fabric weaved from threads in one direction, respect normal map, also works well for layer of glass panels.\n" - " - Anisotropic Fabric: Common fabric weaved from tangent and birnormal direction, ignores normal map.\n"); + ImGui::Text("%s", T(TKEY("default_material_model_tooltip"), + "Anisotropic translucency will adjust the opacity based on your view angle to the translucent surface.\n" + " - Disabled: No anisotropic translucency, flat alpha.\n" + " - Rim Edge: Naive rim light effect with no physics model, the edge of the geometry is always opaque even its full transparent.\n" + " - Isotropic Fabric: Imaginary fabric weaved from threads in one direction, respect normal map, also works well for layer of glass panels.\n" + " - Anisotropic Fabric: Common fabric weaved from tangent and birnormal direction, ignores normal map.\n")); } - if (ImGui::Checkbox("Skinned Mesh Only", &settings.SkinnedOnly)) { + if (ImGui::Checkbox(T(TKEY("skinned_mesh_only"), "Skinned Mesh Only"), &settings.SkinnedOnly)) { changed = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Control if this effect should only apply to skinned mesh, check this option if your are seeing undesired effect on random objects."); + ImGui::Text("%s", T(TKEY("skinned_mesh_only_tooltip"), "Control if this effect should only apply to skinned mesh. Check this option if you are seeing undesired effects on random objects.")); } - if (ImGui::SliderFloat("Transparency Increase", &settings.AlphaReduction, 0, 1.f)) { + if (ImGui::SliderFloat(T(TKEY("transparency_increase"), "Transparency Increase"), &settings.AlphaReduction, 0, 1.f)) { changed = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Transluent material will make the material more opaque on average, which could be different from the intent, reduce the alpha to counter this effect and increase the dynamic range of the output."); + ImGui::Text("%s", T(TKEY("transparency_increase_tooltip"), "Translucent material will make the material more opaque on average, which could be different from the intent. Reduce the alpha to counter this effect and increase the dynamic range of the output.")); } - if (ImGui::SliderFloat("Softness", &settings.AlphaSoftness, 0.0f, 1.0f)) { + if (ImGui::SliderFloat(T(TKEY("softness"), "Softness"), &settings.AlphaSoftness, 0.0f, 1.0f)) { changed = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Control the softness of the alpha increase, increase the softness reduce the increased amount of alpha."); + ImGui::Text("%s", T(TKEY("softness_tooltip"), "Control the softness of the alpha increase, increase the softness reduce the increased amount of alpha.")); } - if (ImGui::SliderFloat("Blend Weight", &settings.AlphaStrength, 0.0f, 1.0f)) { + if (ImGui::SliderFloat(T(TKEY("blend_weight"), "Blend Weight"), &settings.AlphaStrength, 0.0f, 1.0f)) { changed = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Control the blend weight of the effect applied to the final result."); + ImGui::Text("%s", T(TKEY("blend_weight_tooltip"), "Control the blend weight of the effect applied to the final result.")); } ImGui::Spacing(); @@ -155,15 +158,4 @@ void ExtendedTranslucency::RestoreDefaultSettings() settings = {}; } -std::pair> ExtendedTranslucency::GetFeatureSummary() -{ - return { - "Extended Translucency provides realistic rendering of thin fabric and other translucent materials.\n" - "This feature supports multiple material models for different types of translucent surfaces.", - { "Multiple translucency material models (rim edge, isotropic/anisotropic fabric)", - "Realistic fabric translucency with directional light transmission", - "Per-material override support via NIF extra data", - "Configurable transparency and softness controls", - "Performance-optimized translucency calculations" } - }; -} +#undef I18N_KEY_PREFIX diff --git a/src/Features/ExtendedTranslucency.h b/src/Features/ExtendedTranslucency.h index 88686e87d0..fd40a8f0cb 100644 --- a/src/Features/ExtendedTranslucency.h +++ b/src/Features/ExtendedTranslucency.h @@ -6,10 +6,19 @@ struct ExtendedTranslucency final : Feature { virtual inline std::string GetName() override { return "Extended Translucency"; } + virtual std::string GetDisplayName() override { return T("feature.extended_translucency.name", "Extended Translucency"); } virtual inline std::string GetShortName() override { return "ExtendedTranslucency"; } virtual inline std::string_view GetShaderDefineName() override { return "EXTENDED_TRANSLUCENCY"sv; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } - virtual std::pair> GetFeatureSummary() override; + virtual std::pair> GetFeatureSummary() override + { + return { T("feature.extended_translucency.description", "Extended Translucency provides realistic rendering of thin fabric and other translucent materials.\nThis feature supports multiple material models for different types of translucent surfaces."), + { T("feature.extended_translucency.key_feature_1", "Multiple translucency material models (rim edge, isotropic/anisotropic fabric)"), + T("feature.extended_translucency.key_feature_2", "Realistic fabric translucency with directional light transmission"), + T("feature.extended_translucency.key_feature_3", "Per-material override support via NIF extra data"), + T("feature.extended_translucency.key_feature_4", "Configurable transparency and softness controls"), + T("feature.extended_translucency.key_feature_5", "Performance-optimized translucency calculations") } }; + } virtual bool HasShaderDefine(RE::BSShader::Type shaderType) override { return RE::BSShader::Type::Lighting == shaderType; }; virtual void PostPostLoad() override; virtual void DrawSettings() override; diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index 6f0feae96c..8f98fea1cb 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -1,10 +1,13 @@ #include "GrassCollision.h" #include "Globals.h" +#include "I18n/I18n.h" #include "State.h" #include "Utils/ActorUtils.h" #include "Utils/D3D.h" +#define I18N_KEY_PREFIX "feature.grass_collision." + static constexpr uint MAX_BOUNDING_BOXES = 64; static constexpr uint MAX_COLLISIONS_PER_BOUNDING_BOX = 64; static constexpr uint MAX_COLLISIONS = MAX_BOUNDING_BOXES * MAX_COLLISIONS_PER_BOUNDING_BOX; @@ -25,8 +28,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void GrassCollision::DrawSettings() { - if (ImGui::TreeNodeEx("Grass Collision", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Grass Collision", (bool*)&settings.EnableGrassCollision); + if (ImGui::TreeNodeEx(T(TKEY("grass_collision"), "Grass Collision"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable"), "Enable Grass Collision"), (bool*)&settings.EnableGrassCollision); ImGui::TreePop(); } } @@ -410,3 +413,4 @@ void GrassCollision::UpdateCollisionTexture() ID3D11UnorderedAccessView* null_uavs[1] = { nullptr }; context->CSSetUnorderedAccessViews(0, 1, null_uavs, nullptr); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/GrassCollision.h b/src/Features/GrassCollision.h index 6e5e7cd508..da25c5e150 100644 --- a/src/Features/GrassCollision.h +++ b/src/Features/GrassCollision.h @@ -6,21 +6,20 @@ struct GrassCollision : Feature { public: virtual inline std::string GetName() override { return "Grass Collision"; } + virtual std::string GetDisplayName() override { return T("feature.grass_collision.name", "Grass Collision"); } virtual inline std::string GetShortName() override { return "GrassCollision"; } virtual inline std::string_view GetShaderDefineName() override { return "GRASS_COLLISION"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kGrass; } virtual std::pair> GetFeatureSummary() override { - return { - "Enables dynamic grass interactions where grass bends and moves in response to actors walking through it, creating more immersive environmental reactions.", - { "Real-time grass deformation from actor movement", - "Collision detection for up to 256 simultaneous interactions", - "Dynamic tracking of actor positions for grass response", - "Performance-optimized collision calculation", - "Seamless integration with existing grass rendering" } - }; - } + return { T("feature.grass_collision.description", "Enables dynamic grass interactions where grass bends and moves in response to actors walking through it, creating more immersive environmental reactions."), + { T("feature.grass_collision.key_feature_1", "Real-time grass deformation from actor movement"), + T("feature.grass_collision.key_feature_2", "Collision detection for up to 256 simultaneous interactions"), + T("feature.grass_collision.key_feature_3", "Dynamic tracking of actor positions for grass response"), + T("feature.grass_collision.key_feature_4", "Performance-optimized collision calculation"), + T("feature.grass_collision.key_feature_5", "Seamless integration with existing grass rendering") } }; + }; bool HasShaderDefine(RE::BSShader::Type shaderType) override; diff --git a/src/Features/GrassLighting.cpp b/src/Features/GrassLighting.cpp index 59054832c9..24ea27f4b1 100644 --- a/src/Features/GrassLighting.cpp +++ b/src/Features/GrassLighting.cpp @@ -1,5 +1,9 @@ #include "GrassLighting.h" +#include "I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.grass_lighting." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( GrassLighting::Settings, Glossiness, @@ -11,24 +15,24 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void GrassLighting::DrawSettings() { - if (ImGui::TreeNodeEx("Complex Grass", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Specular highlights for complex grass"); - ImGui::SliderFloat("Glossiness", &settings.Glossiness, 1.0f, 100.0f); + if (ImGui::TreeNodeEx(T(TKEY("complex_grass"), "Complex Grass"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextWrapped("%s", T(TKEY("specular_desc"), "Specular highlights for complex grass")); + ImGui::SliderFloat(T(TKEY("glossiness"), "Glossiness"), &settings.Glossiness, 1.0f, 100.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Specular highlight glossiness."); + ImGui::Text("%s", T(TKEY("glossiness_tooltip"), "Specular highlight glossiness.")); } - ImGui::SliderFloat("Specular Strength", &settings.SpecularStrength, 0.0f, 1.0f); + ImGui::SliderFloat(T(TKEY("specular_strength"), "Specular Strength"), &settings.SpecularStrength, 0.0f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Specular highlight strength."); + ImGui::Text("%s", T(TKEY("specular_strength_tooltip"), "Specular highlight strength.")); } ImGui::Spacing(); - ImGui::TextWrapped("Complex Grass Detection"); - ImGui::SliderFloat("Detection Threshold", &settings.ComplexGrassThreshold, 0.001f, 0.1f, "%.3f"); + ImGui::TextWrapped("%s", T(TKEY("detection_header"), "Complex Grass Detection")); + ImGui::SliderFloat(T(TKEY("detection_threshold"), "Detection Threshold"), &settings.ComplexGrassThreshold, 0.001f, 0.1f, "%.3f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Threshold for detecting complex grass textures. Lower values are more strict."); + ImGui::Text("%s", T(TKEY("detection_threshold_tooltip"), + "Threshold for detecting complex grass textures. Lower values are more strict.")); } ImGui::Spacing(); @@ -36,14 +40,14 @@ void GrassLighting::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Effects", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("SSS Amount", &settings.SubsurfaceScatteringAmount, 0.0f, 1.0f); + if (ImGui::TreeNodeEx(T(TKEY("effects"), "Effects"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("sss_amount"), "SSS Amount"), &settings.SubsurfaceScatteringAmount, 0.0f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Subsurface Scattering (SSS) amount. " - "Soft lighting controls how evenly lit an object is. " - "Back lighting illuminates the back face of an object. " - "Combined to model the transport of light through the surface. "); + ImGui::Text("%s", T(TKEY("sss_tooltip"), + "Subsurface Scattering (SSS) amount. " + "Soft lighting controls how evenly lit an object is. " + "Back lighting illuminates the back face of an object. " + "Combined to model the transport of light through the surface.")); } ImGui::Spacing(); @@ -51,28 +55,30 @@ void GrassLighting::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Override Complex Grass Lighting Settings", (bool*)&settings.OverrideComplexGrassSettings); + if (ImGui::TreeNodeEx(T(TKEY("lighting"), "Lighting"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("override_complex"), "Override Complex Grass Lighting Settings"), (bool*)&settings.OverrideComplexGrassSettings); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Override the settings set by the grass mesh author. " - "Complex grass authors can define the brightness for their grass meshes. " - "However, some authors may not account for the extra lights available from Community Shaders. " - "This option will treat their grass settings like non-complex grass. " - "This was the default in Community Shaders < 0.7.0"); + ImGui::Text("%s", T(TKEY("override_complex_tooltip"), + "Override the settings set by the grass mesh author. " + "Complex grass authors can define the brightness for their grass meshes. " + "However, some authors may not account for the extra lights available from Community Shaders. " + "This option will treat their grass settings like non-complex grass. " + "This was the default in Community Shaders < 0.7.0")); } ImGui::Spacing(); ImGui::Spacing(); - ImGui::TextWrapped("Basic Grass"); - ImGui::SliderFloat("Brightness", &settings.BasicGrassBrightness, 0.0f, 1.0f); + ImGui::TextWrapped("%s", T(TKEY("basic_grass"), "Basic Grass")); + ImGui::SliderFloat(T(TKEY("brightness"), "Brightness"), &settings.BasicGrassBrightness, 0.0f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Darkens the grass textures to look better with the new lighting"); + ImGui::Text("%s", T(TKEY("brightness_tooltip"), "Darkens the grass textures to look better with the new lighting")); } ImGui::TreePop(); } } +#undef I18N_KEY_PREFIX + void GrassLighting::LoadSettings(json& o_json) { settings = o_json; diff --git a/src/Features/GrassLighting.h b/src/Features/GrassLighting.h index a4723a9ac9..61bacc67ec 100644 --- a/src/Features/GrassLighting.h +++ b/src/Features/GrassLighting.h @@ -6,6 +6,7 @@ struct GrassLighting : Feature { public: virtual inline std::string GetName() override { return "Grass Lighting"; } + virtual std::string GetDisplayName() override { return T("feature.grass_lighting.name", "Grass Lighting"); } virtual inline std::string GetShortName() override { return "GrassLighting"; } virtual inline std::string_view GetShaderDefineName() override { return "GRASS_LIGHTING"; } virtual bool HasShaderDefine(RE::BSShader::Type shaderType) override { return shaderType == RE::BSShader::Type::Grass; }; @@ -13,16 +14,13 @@ struct GrassLighting : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Grass Lighting enhances grass rendering with improved lighting, specularity, and subsurface scattering.\n" - "This makes grass appear more natural and responsive to lighting conditions.", - { "Enhanced grass lighting model", - "Specular highlights on grass", - "Subsurface scattering effects", - "Improved grass visual quality", - "Configurable material properties" } - }; - } + return { T("feature.grass_lighting.description", "Grass Lighting enhances grass rendering with improved lighting, specularity, and subsurface scattering.\nThis makes grass appear more natural and responsive to lighting conditions."), + { T("feature.grass_lighting.key_feature_1", "Enhanced grass lighting model"), + T("feature.grass_lighting.key_feature_2", "Specular highlights on grass"), + T("feature.grass_lighting.key_feature_3", "Subsurface scattering effects"), + T("feature.grass_lighting.key_feature_4", "Improved grass visual quality"), + T("feature.grass_lighting.key_feature_5", "Configurable material properties") } }; + }; struct alignas(16) Settings { diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 1e01f66e2a..8a944c32ee 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -4,6 +4,7 @@ #include "Buffer.h" #include "Globals.h" +#include "I18n/I18n.h" #include "LinearLighting.h" #include "Menu.h" #include "ShaderCache.h" @@ -14,6 +15,8 @@ #include #include +#define I18N_KEY_PREFIX "feature.hdr_display." + // Win11 24H2 display config types. Compat_ prefix avoids collision with SDK enum members. typedef enum { @@ -294,24 +297,26 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void HDRDisplay::DrawSettings() { + auto hdrWarningPopupTitle = std::format("{}##HDRDisplay", T(TKEY("warning_popup_title"), "HDR Warning")); + if (isHDRMonitor) { - Util::Text::Success("HDR Display Detected"); + Util::Text::Success(T(TKEY("display_detected"), "HDR Display Detected")); } else if (isHDRCapableMonitor) { - Util::Text::Warning("HDR Capable Display (Windows HDR is off)"); + Util::Text::Warning(T(TKEY("capable_display_windows_hdr_off"), "HDR Capable Display (Windows HDR is off)")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Your monitor supports HDR, but Windows HDR is currently disabled."); - ImGui::Text("Enable HDR in Windows Display Settings to allow auto-detection."); + ImGui::TextUnformatted(T(TKEY("capable_display_windows_hdr_off_tooltip_0"), "Your monitor supports HDR, but Windows HDR is currently disabled.")); + ImGui::TextUnformatted(T(TKEY("capable_display_windows_hdr_off_tooltip_1"), "Enable HDR in Windows Display Settings to allow auto-detection.")); } } else { - Util::Text::Warning("SDR Display (HDR not detected)"); + Util::Text::Warning(T(TKEY("sdr_display_not_detected"), "SDR Display (HDR not detected)")); } const bool isExclusiveFullscreen = globals::features::upscaling.loaded ? !globals::features::upscaling.isWindowed : wasExclusiveFullscreen; if (isExclusiveFullscreen) { ImGui::Spacing(); - Util::Text::WrappedWarning("WARNING: Exclusive Fullscreen detected."); - Util::Text::WrappedWarning("HDR is not compatible with Exclusive Fullscreen and may not work correctly. Switch to Borderless Windowed mode for proper HDR support."); + Util::Text::WrappedWarning(T(TKEY("exclusive_fullscreen_warning"), "WARNING: Exclusive Fullscreen detected.")); + Util::Text::WrappedWarning(T(TKEY("exclusive_fullscreen_warning_detail"), "HDR is not compatible with Exclusive Fullscreen and may not work correctly. Switch to Borderless Windowed mode for proper HDR support.")); ImGui::Spacing(); } @@ -332,7 +337,7 @@ void HDRDisplay::DrawSettings() ImGui::BeginDisabled(); } - if (ImGui::Checkbox("Enable HDR", ¤tEnableHDR)) { + if (ImGui::Checkbox(T(TKEY("enable_hdr"), "Enable HDR"), ¤tEnableHDR)) { { std::lock_guard lock(settingsMutex); settings.enableHDR = currentEnableHDR; @@ -354,18 +359,18 @@ void HDRDisplay::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) { if (isHDRMonitor) { - ImGui::Text("Enable HDR output. Matches vanilla visuals with extended dynamic range."); + ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip"), "Enable HDR output. Matches vanilla visuals with extended dynamic range.")); } else if (isHDRCapableMonitor) { - ImGui::Text("Monitor supports HDR but Windows HDR is off. Enable HDR in Windows Display Settings, then restart the game."); + ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip_windows_off"), "Monitor supports HDR but Windows HDR is off. Enable HDR in Windows Display Settings, then restart the game.")); } else { - ImGui::Text("HDR display not detected. Use Advanced button to override."); + ImGui::TextUnformatted(T(TKEY("enable_hdr_tooltip_not_detected"), "HDR display not detected. Use Advanced button to override.")); } } // Advanced override button — shown when HDR is neither active nor auto-detected if (!isHDRMonitor && !oldEnableHDR) { ImGui::SameLine(); - if (ImGui::Button("Advanced")) { + if (ImGui::Button(T(TKEY("advanced"), "Advanced"))) { bool dontShowWarning; { std::lock_guard lock(settingsMutex); @@ -374,7 +379,7 @@ void HDRDisplay::DrawSettings() if (!dontShowWarning) { pendingHDREnable = true; showHDRWarningPopup = true; - ImGui::OpenPopup("HDR Warning##HDRDisplay"); + ImGui::OpenPopup(hdrWarningPopupTitle.c_str()); } else { // User previously dismissed warnings, enable directly { @@ -388,9 +393,9 @@ void HDRDisplay::DrawSettings() } if (auto _tt = Util::HoverTooltipWrapper()) { if (isHDRCapableMonitor) { - ImGui::Text("Enable Windows HDR instead of forcing it here."); + ImGui::TextUnformatted(T(TKEY("advanced_tooltip_enable_windows_hdr"), "Enable Windows HDR instead of forcing it here.")); } else { - ImGui::Text("Force enable HDR even without detection (not recommended)."); + ImGui::TextUnformatted(T(TKEY("advanced_tooltip_force_enable"), "Force enable HDR even without detection (not recommended).")); } } } @@ -400,26 +405,26 @@ void HDRDisplay::DrawSettings() std::lock_guard lock(settingsMutex); if (!isHDRMonitor && settings.enableHDR) { ImGui::Spacing(); - Util::Text::WrappedWarning("HDR is enabled but no HDR display was detected."); + Util::Text::WrappedWarning(T(TKEY("enabled_without_detected_display"), "HDR is enabled but no HDR display was detected.")); } } - if (auto popup = Util::CenteredPopupModal("HDR Warning##HDRDisplay", &showHDRWarningPopup, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + if (auto popup = Util::CenteredPopupModal(hdrWarningPopupTitle.c_str(), &showHDRWarningPopup, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { // Prevent background dimming by pushing lower modal dimming ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1.0f); - Util::Text::Warning("WARNING: Force Enable HDR"); + Util::Text::Warning(T(TKEY("force_enable_hdr_warning"), "WARNING: Force Enable HDR")); ImGui::Separator(); ImGui::Spacing(); - Util::Text::WrappedWarning("HDR was not detected on your monitor."); - Util::Text::WrappedWarning("The game will look VERY WRONG on an SDR (standard) display."); + Util::Text::WrappedWarning(T(TKEY("force_enable_hdr_detected_warning"), "HDR was not detected on your monitor.")); + Util::Text::WrappedWarning(T(TKEY("force_enable_hdr_sdr_warning"), "The game will look VERY WRONG on an SDR (standard) display.")); ImGui::Spacing(); - ImGui::TextWrapped("Only proceed if you have an HDR-capable display that was not detected correctly."); + ImGui::TextWrapped("%s", T(TKEY("force_enable_hdr_confirm"), "Only proceed if you have an HDR-capable display that was not detected correctly.")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button("Force Enable HDR", ImVec2(150, 0))) { + if (ImGui::Button(T(TKEY("force_enable_hdr"), "Force Enable HDR"), ImVec2(150, 0))) { { std::lock_guard lock(settingsMutex); settings.enableHDR = true; @@ -432,7 +437,7 @@ void HDRDisplay::DrawSettings() ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(150, 0))) { + if (ImGui::Button(T(TKEY("cancel"), "Cancel"), ImVec2(150, 0))) { { std::lock_guard lock(settingsMutex); settings.enableHDR = false; @@ -454,7 +459,7 @@ void HDRDisplay::DrawSettings() } ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(ImGui::GetStyle().FramePadding.x, ImGui::GetStyle().FramePadding.y * 0.5f)); ImGui::SetWindowFontScale(0.9f); - if (ImGui::Checkbox("Don't show me this again", &dontShowWarning)) { + if (ImGui::Checkbox(T(TKEY("dont_show_again"), "Don't show me this again"), &dontShowWarning)) { std::lock_guard lock(settingsMutex); settings.dontShowHDRWarning = dontShowWarning; } @@ -486,7 +491,7 @@ void HDRDisplay::DrawSettings() currentPeakNits = settings.hdrPeakNits; } - ImGui::SliderInt("Paper White (nits)", reinterpret_cast(¤tPaperWhite), 80, 500); + ImGui::SliderInt(T(TKEY("paper_white_nits"), "Paper White (nits)"), reinterpret_cast(¤tPaperWhite), 80, 500); { std::lock_guard lock(settingsMutex); if (currentPaperWhite >= settings.hdrPeakNits) { @@ -498,11 +503,11 @@ void HDRDisplay::DrawSettings() } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How bright SDR white appears on your HDR display."); - ImGui::Text("203 nits is the ITU BT.2408 reference. Increase for a brighter image."); + ImGui::TextUnformatted(T(TKEY("paper_white_tooltip_0"), "How bright SDR white appears on your HDR display.")); + ImGui::TextUnformatted(T(TKEY("paper_white_tooltip_1"), "203 nits is the ITU BT.2408 reference. Increase for a brighter image.")); } - ImGui::SliderInt("Peak Brightness (nits)", reinterpret_cast(¤tPeakNits), 400, 10000); + ImGui::SliderInt(T(TKEY("peak_brightness_nits"), "Peak Brightness (nits)"), reinterpret_cast(¤tPeakNits), 400, 10000); { std::lock_guard lock(settingsMutex); if (currentPeakNits <= settings.hdrPaperWhite) { @@ -514,15 +519,15 @@ void HDRDisplay::DrawSettings() } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Maximum brightness your display can produce."); - ImGui::Text("Set to match your display's actual peak brightness."); + ImGui::TextUnformatted(T(TKEY("peak_brightness_tooltip_0"), "Maximum brightness your display can produce.")); + ImGui::TextUnformatted(T(TKEY("peak_brightness_tooltip_1"), "Set to match your display's actual peak brightness.")); } - ImGui::TextDisabled("Display reports: %.0f nits max", cachedDisplayMaxLuminance); + ImGui::TextDisabled(T(TKEY("display_reports_max_nits"), "Display reports: %.0f nits max"), cachedDisplayMaxLuminance); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Reported by OS/driver (DXGI MaxLuminance), not a direct meter reading."); - ImGui::Text("It may be EDID metadata and can differ from real highlight peak output."); - ImGui::Text("Treat this as a starting point and tune Peak Brightness as needed."); + ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_0"), "Reported by OS/driver (DXGI MaxLuminance), not a direct meter reading.")); + ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_1"), "It may be EDID metadata and can differ from real highlight peak output.")); + ImGui::TextUnformatted(T(TKEY("display_reports_max_nits_tooltip_2"), "Treat this as a starting point and tune Peak Brightness as needed.")); } } @@ -534,20 +539,22 @@ void HDRDisplay::DrawSettings() float oldUIBrightness = settings.hdrUIBrightness; float currentUIBrightness = settings.hdrUIBrightness; - ImGui::SliderFloat("UI Brightness Multiplier", ¤tUIBrightness, 0.5f, 5.0f, "%.2fx"); + ImGui::SliderFloat(T(TKEY("ui_brightness_multiplier"), "UI Brightness Multiplier"), ¤tUIBrightness, 0.5f, 5.0f, "%.2fx"); if (oldUIBrightness != currentUIBrightness) { settings.hdrUIBrightness = currentUIBrightness; UpdateHDRData(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("UI brightness = Paper White × this multiplier in HDR mode."); - ImGui::Text("1.00x = UI renders at Paper White brightness. Higher values make UI brighter relative to scene content."); - ImGui::Text("Note: Main menu and loading screens always render at Paper White brightness."); + ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_0"), "UI brightness = Paper White x this multiplier in HDR mode.")); + ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_1"), "1.00x = UI renders at Paper White brightness. Higher values make UI brighter relative to scene content.")); + ImGui::TextUnformatted(T(TKEY("ui_brightness_multiplier_tooltip_2"), "Note: Main menu and loading screens always render at Paper White brightness.")); } } } } +#undef I18N_KEY_PREFIX + void HDRDisplay::SaveSettings(json& o_json) { std::lock_guard lock(settingsMutex); @@ -877,7 +884,7 @@ void HDRDisplay::SetUIBuffer() ID3D11RenderTargetView* targetRTV = uiBufferMode.useUIBuffer ? upscaling.dx12SwapChain.uiBufferWrapped->rtv : uiBufferMode.useFallbackCopy ? fb.RTV : - upscaling.dx12SwapChain.swapChainBufferWrapped->rtv; + upscaling.dx12SwapChain.swapChainBufferWrapped->rtv; if (uiBufferMode.useUIBuffer) { float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; @@ -991,9 +998,9 @@ ID3D11BlendState* HDRDisplay::GetPatchedAlphaBlendState(ID3D11BlendState* origin for (int i = 0; i < slotCount; i++) { const auto& rt = desc.RenderTarget[i]; if (rt.BlendEnable && - (rt.SrcBlendAlpha != D3D11_BLEND_ONE || - rt.DestBlendAlpha != D3D11_BLEND_INV_SRC_ALPHA || - rt.BlendOpAlpha != D3D11_BLEND_OP_ADD)) { + (rt.SrcBlendAlpha != D3D11_BLEND_ONE || + rt.DestBlendAlpha != D3D11_BLEND_INV_SRC_ALPHA || + rt.BlendOpAlpha != D3D11_BLEND_OP_ADD)) { needsPatch = true; break; } diff --git a/src/Features/HDRDisplay.h b/src/Features/HDRDisplay.h index 5359ce1d16..e5cc29b58f 100644 --- a/src/Features/HDRDisplay.h +++ b/src/Features/HDRDisplay.h @@ -16,6 +16,7 @@ struct HDRDisplay : public Feature public: virtual inline std::string GetName() override { return "HDR Display"; } + virtual std::string GetDisplayName() override { return T("feature.hdr_display.name", "HDR Display"); } virtual inline std::string GetShortName() override { return "HDRDisplay"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetCategory() const override { return "Display"; } @@ -30,15 +31,11 @@ struct HDRDisplay : public Feature virtual std::pair> GetFeatureSummary() override { - return { - "Real High Dynamic Range output for HDR displays.", - { - "HDR10 output support (10-bit) with upgraded HDR buffers (16-Bit), and fully unclamped rendering pipeline for true HDR values.", - "HDR-aware tonemapping based on Skyrim's ISHDR path (Reinhard/Hejl-Burgess-Dawson), preserving the vanilla look while improving highlight handling on HDR displays.", - "Configurable paper white and peak brightness.", - } - }; - } + return { T("feature.hdr_display.description", "Real High Dynamic Range output for HDR displays."), + { T("feature.hdr_display.key_feature_1", "HDR10 output support (10-bit) with upgraded HDR buffers (16-Bit), and fully unclamped rendering pipeline for true HDR values."), + T("feature.hdr_display.key_feature_2", "HDR-aware tonemapping based on Skyrim's ISHDR path (Reinhard/Hejl-Burgess-Dawson), preserving the vanilla look while improving highlight handling on HDR displays."), + T("feature.hdr_display.key_feature_3", "Configurable paper white and peak brightness.") } }; + }; struct Settings { diff --git a/src/Features/HairSpecular.cpp b/src/Features/HairSpecular.cpp index 3d09a5e66b..52305c2c65 100644 --- a/src/Features/HairSpecular.cpp +++ b/src/Features/HairSpecular.cpp @@ -1,8 +1,11 @@ #include "HairSpecular.h" +#include "../I18n/I18n.h" #include "Utils/D3D.h" #include +#define I18N_KEY_PREFIX "feature.hair_specular." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( HairSpecular::Settings, Enabled, @@ -25,52 +28,52 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void HairSpecular::DrawSettings() { - ImGui::Checkbox("Enabled", (bool*)&settings.Enabled); - ImGui::Combo("Hair Mode", (int*)&settings.HairMode, "Kajiya-Kay\0Marschner\0"); + ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), (bool*)&settings.Enabled); + ImGui::Combo(T(TKEY("hair_mode"), "Hair Mode"), (int*)&settings.HairMode, "Kajiya-Kay\0Marschner\0"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Select the hair shading model to use.\n" - "Kajiya-Kay is an empirical model that simulates hair specular highlights.\n" - "Marschner is a more physically-based model that simulates hair light interaction.\n" - "Both models are anisotropic and support tangent-based shading.\n" - "Without self-shadowing, Marschner may look overly bright because of transmission.\n"); + ImGui::Text("%s", T(TKEY("hair_mode_tooltip"), + "Select the hair shading model to use.\n" + "Kajiya-Kay is an empirical model that simulates hair specular highlights.\n" + "Marschner is a more physically-based model that simulates hair light interaction.\n" + "Both models are anisotropic and support tangent-based shading.\n" + "Without self-shadowing, Marschner may look overly bright because of transmission.\n")); } ImGui::Spacing(); - ImGui::SliderFloat("Glossiness", &settings.HairGlossiness, 0.0f, settings.HairMode == 0 ? 256.0f : 100.0f, "%.0f"); + ImGui::SliderFloat(T(TKEY("glossiness"), "Glossiness"), &settings.HairGlossiness, 0.0f, settings.HairMode == 0 ? 256.0f : 100.0f, "%.0f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Controls the glossiness of the hair.\n" - "Glossiness in Kajiya-Kay mode maps to the specular exponent.\n" - "In Marschner mode, it controls the roughness of the hair surface.\n"); + ImGui::Text("%s", T(TKEY("glossiness_tooltip"), + "Controls the glossiness of the hair.\n" + "Glossiness in Kajiya-Kay mode maps to the specular exponent.\n" + "In Marschner mode, it controls the roughness of the hair surface.\n")); } - ImGui::SliderFloat("Specular Multiplier", &settings.SpecularMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Diffuse Multiplier", &settings.DiffuseMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Indirect Specular Multiplier", &settings.SpecularIndirectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Indirect Diffuse Multiplier", &settings.DiffuseIndirectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Hair Base Color Multiplier", &settings.BaseColorMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Hair Saturation", &settings.HairSaturation, 0.0f, 5.0f, "%.2f"); - ImGui::SliderFloat("Transmission", &settings.Transmission, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("specular_multiplier"), "Specular Multiplier"), &settings.SpecularMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("diffuse_multiplier"), "Diffuse Multiplier"), &settings.DiffuseMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("indirect_specular_multiplier"), "Indirect Specular Multiplier"), &settings.SpecularIndirectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("indirect_diffuse_multiplier"), "Indirect Diffuse Multiplier"), &settings.DiffuseIndirectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("hair_base_color_multiplier"), "Hair Base Color Multiplier"), &settings.BaseColorMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("hair_saturation"), "Hair Saturation"), &settings.HairSaturation, 0.0f, 5.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("transmission"), "Transmission"), &settings.Transmission, 0.0f, 1.0f, "%.2f"); ImGui::Spacing(); - ImGui::Checkbox("Enable Tangent Shift", (bool*)&settings.EnableTangentShift); + ImGui::Checkbox(T(TKEY("enable_tangent_shift"), "Enable Tangent Shift"), (bool*)&settings.EnableTangentShift); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Enables the use of a tangent shift texture to vary specular highlights across hair strands.\n" - "Result may vary based on the hair model used.\n"); + ImGui::Text("%s", T(TKEY("enable_tangent_shift_tooltip"), + "Enables the use of a tangent shift texture to vary specular highlights across hair strands.\n" + "Result may vary based on the hair model used.\n")); } if (settings.HairMode == 0) { - ImGui::SliderFloat("Primary Specular Tangent Shift", &settings.PrimaryTangentShift, -1.0f, 1.0f, "%.2f"); - ImGui::SliderFloat("Secondary Specular Tangent Shift", &settings.SecondaryTangentShift, -1.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("primary_tangent_shift"), "Primary Specular Tangent Shift"), &settings.PrimaryTangentShift, -1.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("secondary_tangent_shift"), "Secondary Specular Tangent Shift"), &settings.SecondaryTangentShift, -1.0f, 1.0f, "%.2f"); } ImGui::Spacing(); - ImGui::Checkbox("Enable Screen-Space Self Shadow", (bool*)&settings.EnableSelfShadow); + ImGui::Checkbox(T(TKEY("enable_self_shadow"), "Enable Screen-Space Self Shadow"), (bool*)&settings.EnableSelfShadow); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Enables screen-space self-shadowing for hair.\n" - "Marschner hair model might have overly bright transmission without self-shadowing.\n"); + ImGui::Text("%s", T(TKEY("enable_self_shadow_tooltip"), + "Enables screen-space self-shadowing for hair.\n" + "Marschner hair model might have overly bright transmission without self-shadowing.\n")); } - ImGui::SliderFloat("Self Shadow Strength", &settings.SelfShadowStrength, 0.0f, 1.0f, "%.2f"); - ImGui::SliderFloat("Self Shadow Exponent", &settings.SelfShadowExponent, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Self Shadow Scale", &settings.SelfShadowScale, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("self_shadow_strength"), "Self Shadow Strength"), &settings.SelfShadowStrength, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("self_shadow_exponent"), "Self Shadow Exponent"), &settings.SelfShadowExponent, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("self_shadow_scale"), "Self Shadow Scale"), &settings.SelfShadowScale, 0.0f, 10.0f, "%.2f"); } void HairSpecular::LoadSettings(json& o_json) @@ -135,4 +138,6 @@ void HairSpecular::Prepass() ID3D11ShaderResourceView* srv = texTangentShift->srv.get(); context->PSSetShaderResources(73, 1, &srv); } -} \ No newline at end of file +} + +#undef I18N_KEY_PREFIX \ No newline at end of file diff --git a/src/Features/HairSpecular.h b/src/Features/HairSpecular.h index 5c7aeda8b9..28fab821ec 100644 --- a/src/Features/HairSpecular.h +++ b/src/Features/HairSpecular.h @@ -7,19 +7,19 @@ struct HairSpecular : Feature public: virtual inline std::string GetName() override { return "Hair Specular"; } + virtual std::string GetDisplayName() override { return T("feature.hair_specular.name", "Hair Specular"); } virtual inline std::string GetShortName() override { return "HairSpecular"; } virtual inline std::string_view GetShaderDefineName() override { return "CS_HAIR"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kCharacters; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides better hair shading with realistic specular highlights and tangent-based light interaction for more lifelike hair appearance.", - { "Realistic hair specular highlights", - "Enhanced hair glossiness and saturation controls", - "Separate specular and diffuse lighting multipliers", - "Tangent shift texture support for varied hair highlights" } - }; - } + return { T("feature.hair_specular.description", "Provides better hair shading with realistic specular highlights and tangent-based light interaction for more lifelike hair appearance."), + { T("feature.hair_specular.key_feature_1", "Realistic hair specular highlights"), + T("feature.hair_specular.key_feature_2", "Enhanced hair glossiness and saturation controls"), + T("feature.hair_specular.key_feature_3", "Separate specular and diffuse lighting multipliers"), + T("feature.hair_specular.key_feature_4", "Tangent shift texture support for varied hair highlights") } }; + }; + virtual bool HasShaderDefine(RE::BSShader::Type shaderType) override { return shaderType == RE::BSShader::Type::Lighting; }; virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } diff --git a/src/Features/IBL.cpp b/src/Features/IBL.cpp index f0f82a0140..d93aff525f 100644 --- a/src/Features/IBL.cpp +++ b/src/Features/IBL.cpp @@ -6,9 +6,12 @@ #include "State.h" #include "WeatherVariableRegistry.h" +#include "../I18n/I18n.h" #include #include +#define I18N_KEY_PREFIX "feature.ibl." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( IBL::Settings, EnableIBL, @@ -25,65 +28,72 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void IBL::DrawSettings() { - Util::WeatherUI::Checkbox("Enable IBL", this, "EnableIBL", (bool*)&settings.EnableIBL); + Util::WeatherUI::Checkbox(T(TKEY("enable_ibl"), "Enable IBL"), this, "EnableIBL", (bool*)&settings.EnableIBL); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Toggle IBL. When enabled, ambient lighting is derived from cubemap spherical harmonics instead of the vanilla system."); + ImGui::Text("%s", T(TKEY("enable_ibl_tooltip"), "Toggle IBL. When enabled, ambient lighting is derived from cubemap spherical harmonics instead of the vanilla system.")); } - Util::WeatherUI::SliderFloat("Env IBL Scale", this, "EnvIBLScale", &settings.EnvIBLScale, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("env_ibl_scale"), "Env IBL Scale"), this, "EnvIBLScale", &settings.EnvIBLScale, 0.0f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Intensity multiplier for the environment IBL (from Dynamic Cubemaps).\nControls how strongly the surrounding environment contributes to ambient lighting."); + ImGui::Text("%s", T(TKEY("env_ibl_scale_tooltip"), "Intensity multiplier for the environment IBL (from Dynamic Cubemaps).\nControls how strongly the surrounding environment contributes to ambient lighting.")); } - Util::WeatherUI::SliderFloat("Sky IBL Scale", this, "SkyIBLScale", &settings.SkyIBLScale, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("sky_ibl_scale"), "Sky IBL Scale"), this, "SkyIBLScale", &settings.SkyIBLScale, 0.0f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Intensity multiplier for the sky IBL (from the game's native reflections cubemap).\nControls how strongly the sky contributes to ambient lighting."); + ImGui::Text("%s", T(TKEY("sky_ibl_scale_tooltip"), "Intensity multiplier for the sky IBL (from the game's native reflections cubemap).\nControls how strongly the sky contributes to ambient lighting.")); } - Util::WeatherUI::SliderFloat("Env IBL Saturation", this, "EnvIBLSaturation", &settings.EnvIBLSaturation, 0.0f, 2.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("env_ibl_saturation"), "Env IBL Saturation"), this, "EnvIBLSaturation", &settings.EnvIBLSaturation, 0.0f, 2.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Color saturation of the environment IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color."); + ImGui::Text("%s", T(TKEY("env_ibl_saturation_tooltip"), "Color saturation of the environment IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color.")); } - Util::WeatherUI::SliderFloat("Sky IBL Saturation", this, "SkyIBLSaturation", &settings.SkyIBLSaturation, 0.0f, 2.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("sky_ibl_saturation"), "Sky IBL Saturation"), this, "SkyIBLSaturation", &settings.SkyIBLSaturation, 0.0f, 2.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Color saturation of the sky IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color."); + ImGui::Text("%s", T(TKEY("sky_ibl_saturation_tooltip"), "Color saturation of the sky IBL.\nLower values produce more neutral ambient light; higher values produce more vivid color.")); } - Util::WeatherUI::SliderFloat("DALC Amount", this, "DALCAmount", &settings.DALCAmount, 0.0f, 1.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("dalc_amount"), "DALC Amount"), this, "DALCAmount", &settings.DALCAmount, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Blends the IBL brightness toward the game's vanilla ambient (DALC) level.\n" - "0 = no matching (pure IBL brightness), 1 = fully matched to vanilla ambient."); + ImGui::Text("%s", T(TKEY("dalc_amount_tooltip"), + "Blends the IBL brightness toward the game's vanilla ambient (DALC) level.\n" + "0 = no matching (pure IBL brightness), 1 = fully matched to vanilla ambient.")); } { - static const char* dalcModeNames[] = { "Luminance Ratio", "Color Ratio", "DALC + Sky", "DALC + Sky (Directional)" }; + const char* dalcModeNames[] = { + T(TKEY("dalc_mode_luminance_ratio"), "Luminance Ratio"), + T(TKEY("dalc_mode_color_ratio"), "Color Ratio"), + T(TKEY("dalc_mode_dalc_plus_sky"), "DALC + Sky"), + T(TKEY("dalc_mode_dalc_plus_sky_directional"), "DALC + Sky (Directional)") + }; int dalcMode = static_cast(settings.DALCMode); - if (ImGui::Combo("DALC Mode", &dalcMode, dalcModeNames, IM_ARRAYSIZE(dalcModeNames))) { + if (ImGui::Combo(T(TKEY("dalc_mode"), "DALC Mode"), &dalcMode, dalcModeNames, IM_ARRAYSIZE(dalcModeNames))) { settings.DALCMode = static_cast(dalcMode); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "How the DALC-to-IBL brightness ratio is computed:\n" - "Luminance Ratio: Scalar ratio from overall luminance (loses DALC color tint).\n" - "Color Ratio: Per-channel ratio (preserves DALC color tint).\n" - "DALC + Sky: Uses vanilla ambient as base, sky IBL on top. Skylighting only affects sky.\n" - "DALC + Sky (Directional): Same, but Skylighting also dims vanilla ambient per-direction."); + ImGui::Text("%s", T(TKEY("dalc_mode_tooltip"), + "How the DALC-to-IBL brightness ratio is computed:\n" + "Luminance Ratio: Scalar ratio from overall luminance (loses DALC color tint).\n" + "Color Ratio: Per-channel ratio (preserves DALC color tint).\n" + "DALC + Sky: Uses vanilla ambient as base, sky IBL on top. Skylighting only affects sky.\n" + "DALC + Sky (Directional): Same, but Skylighting also dims vanilla ambient per-direction.")); } } - ImGui::Checkbox("Use Static IBL For Out-of-World Objects", (bool*)&settings.UseStaticIBL); + ImGui::Checkbox(T(TKEY("use_static_ibl"), "Use Static IBL For Out-of-World Objects"), (bool*)&settings.UseStaticIBL); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Uses pre-baked static IBL cubemap textures for objects rendered outside the game world (e.g. inventory items, loading screens)."); + ImGui::Text("%s", T(TKEY("use_static_ibl_tooltip"), "Uses pre-baked static IBL cubemap textures for objects rendered outside the game world (e.g. inventory items, loading screens).")); } - Util::WeatherUI::SliderFloat("Fog Mix", this, "FogAmount", &settings.FogAmount, 0.0f, 1.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("fog_mix"), "Fog Mix"), this, "FogAmount", &settings.FogAmount, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Blends the fog color toward the IBL ambient color.\n0 = vanilla fog, 1 = fog fully tinted by IBL."); + ImGui::Text("%s", T(TKEY("fog_mix_tooltip"), "Blends the fog color toward the IBL ambient color.\n0 = vanilla fog, 1 = fog fully tinted by IBL.")); } - ImGui::Checkbox("Preserve Fog Luminance", (bool*)&settings.PreserveFogLuminance); + ImGui::Checkbox(T(TKEY("preserve_fog_luminance"), "Preserve Fog Luminance"), (bool*)&settings.PreserveFogLuminance); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When Fog Mix is active, rescales the IBL-tinted fog to keep the original fog brightness.\nPrevents fog from becoming too bright or too dark."); + ImGui::Text("%s", T(TKEY("preserve_fog_luminance_tooltip"), "When Fog Mix is active, rescales the IBL-tinted fog to keep the original fog brightness.\nPrevents fog from becoming too bright or too dark.")); } - ImGui::Checkbox("Disable in interiors", (bool*)&settings.DisableInInteriors); + ImGui::Checkbox(T(TKEY("disable_in_interiors"), "Disable in interiors"), (bool*)&settings.DisableInInteriors); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disables IBL in interior cells."); + ImGui::Text("%s", T(TKEY("disable_in_interiors_tooltip"), "Disables IBL in interior cells.")); } } +#undef I18N_KEY_PREFIX + void IBL::LoadSettings(json& o_json) { settings = o_json; diff --git a/src/Features/IBL.h b/src/Features/IBL.h index d4998982ad..5b153208e4 100644 --- a/src/Features/IBL.h +++ b/src/Features/IBL.h @@ -7,21 +7,20 @@ struct IBL : Feature virtual bool IsCore() const override { return true; }; virtual inline std::string GetName() override { return "Image Based Lighting"; } + virtual std::string GetDisplayName() override { return T("feature.ibl.name", "Image Based Lighting"); } virtual inline std::string GetShortName() override { return "ImageBasedLighting"; } virtual inline std::string_view GetShaderDefineName() override { return "IBL"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Replaces the game's ambient lighting with physically-based IBL derived from cubemap spherical harmonics.", - { "Projects environment and sky cubemaps into spherical harmonics (SH) for irradiance", - "Dual IBL sources: environment cubemap (Dynamic Cubemaps) and Skyrim's native sky reflections cubemap", - "DALC brightness matching to keep IBL consistent with the game's ambient light levels", - "Configurable per-source intensity, saturation, fog mixing, and per-weather overrides", - "Static IBL fallback textures for out-of-world objects (e.g. inventory items)" } - }; - } + return { T("feature.ibl.description", "Replaces the game's ambient lighting with physically-based IBL derived from cubemap spherical harmonics."), + { T("feature.ibl.key_feature_1", "Projects environment and sky cubemaps into spherical harmonics (SH) for irradiance"), + T("feature.ibl.key_feature_2", "Dual IBL sources: environment cubemap (Dynamic Cubemaps) and Skyrim's native sky reflections cubemap"), + T("feature.ibl.key_feature_3", "DALC brightness matching to keep IBL consistent with the game's ambient light levels"), + T("feature.ibl.key_feature_4", "Configurable per-source intensity, saturation, fog mixing, and per-weather overrides"), + T("feature.ibl.key_feature_5", "Static IBL fallback textures for out-of-world objects (e.g. inventory items)") } }; + }; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/InteriorSun.cpp b/src/Features/InteriorSun.cpp index b7965d1566..ca921845b9 100644 --- a/src/Features/InteriorSun.cpp +++ b/src/Features/InteriorSun.cpp @@ -1,6 +1,9 @@ #include "InteriorSun.h" +#include "I18n/I18n.h" #include "State.h" +#define I18N_KEY_PREFIX "feature.interior_sun." + #include NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -10,21 +13,21 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void InteriorSun::DrawSettings() { - ImGui::Checkbox("Force Double-Sided Rendering", &settings.ForceDoubleSidedRendering); + ImGui::Checkbox(T(TKEY("force_double_sided"), "Force Double-Sided Rendering"), &settings.ForceDoubleSidedRendering); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Disables backface culling during sun shadowmap rendering in interiors. " - "Will prevent most light leaking through unmasked/unprepared interiors at a small performance cost. "); + ImGui::Text("%s", T(TKEY("force_double_sided_tooltip"), + "Disables backface culling during sun shadowmap rendering in interiors. " + "Will prevent most light leaking through unmasked/unprepared interiors at a small performance cost. ")); } - if (ImGui::SliderFloat("Interior Shadow Distance", &settings.InteriorShadowDistance, 1000.0f, 8000.0f)) { + if (ImGui::SliderFloat(T(TKEY("interior_shadow_distance"), "Interior Shadow Distance"), &settings.InteriorShadowDistance, 1000.0f, 8000.0f)) { *gInteriorShadowDistance = settings.InteriorShadowDistance; auto tes = RE::TES::GetSingleton(); SetShadowDistance(tes && tes->interiorCell); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Sets the distance shadows are rendered at in interiors. " - "Lower values provide higher quality shadows and improved performance but may cause distant interior spaces to light up incorrectly. "); + ImGui::Text("%s", T(TKEY("interior_shadow_distance_tooltip"), + "Sets the distance shadows are rendered at in interiors. " + "Lower values provide higher quality shadows and improved performance but may cause distant interior spaces to light up incorrectly. ")); } } @@ -221,4 +224,5 @@ void InteriorSun::SetShadowDistance(bool inInterior) using func_t = decltype(SetShadowDistance); static REL::Relocation func{ REL::RelocationID(98978, 105631).address() }; func(inInterior); -} \ No newline at end of file +} +#undef I18N_KEY_PREFIX diff --git a/src/Features/InteriorSun.h b/src/Features/InteriorSun.h index 941d70782f..28918e6769 100644 --- a/src/Features/InteriorSun.h +++ b/src/Features/InteriorSun.h @@ -5,18 +5,18 @@ struct InteriorSun : Feature { public: virtual inline std::string GetName() override { return "Interior Sun"; } + virtual std::string GetDisplayName() override { return T("feature.interior_sun.name", "Interior Sun"); } virtual inline std::string GetShortName() override { return "InteriorSun"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Allows for the sun and moon to cast light and shadows into interior spaces.", - { "Functions only for explicitly enabled interiors", - "Utilizes existing sun, moon, and weather systems", - "Includes an option to force double-sided rendering for unprepared interiors", - "Fixes geometry culling issues that cause light leakage" } - }; - } + return { T("feature.interior_sun.description", "Allows for the sun and moon to cast light and shadows into interior spaces."), + { T("feature.interior_sun.key_feature_1", "Functions only for explicitly enabled interiors"), + T("feature.interior_sun.key_feature_2", "Utilizes existing sun, moon, and weather systems"), + T("feature.interior_sun.key_feature_3", "Includes an option to force double-sided rendering for unprepared interiors"), + T("feature.interior_sun.key_feature_4", "Fixes geometry culling issues that cause light leakage") } }; + }; + virtual void DrawSettings() override; virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; diff --git a/src/Features/InverseSquareLighting.h b/src/Features/InverseSquareLighting.h index fdf65676d0..803ebad13f 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -5,6 +5,7 @@ struct InverseSquareLighting : Feature { public: virtual inline std::string GetName() override { return "Inverse Square Lighting"; } + virtual std::string GetDisplayName() override { return T("feature.inverse_square_lighting.name", "Inverse Square Lighting"); } virtual inline std::string GetShortName() override { return "InverseSquareLighting"; } @@ -14,14 +15,12 @@ struct InverseSquareLighting : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Implements an additional inverse square falloff for lighting which allows for a more physically accurate and realistic looking light attenuation.", - { "Automatic light radius calculation based on intensity", - "Lights smoothly fade out at a configurable cutoff, solving the infinite distance problem", - "Does not modify any existing lighting", - "Requires the use of mods with lights enabled for inverse square falloff.", - "Full integration with Light Placer" } - }; + return { T("feature.inverse_square_lighting.description", "Implements an additional inverse square falloff for lighting which allows for a more physically accurate and realistic looking light attenuation."), + { T("feature.inverse_square_lighting.key_feature_1", "Automatic light radius calculation based on intensity"), + T("feature.inverse_square_lighting.key_feature_2", "Lights smoothly fade out at a configurable cutoff, solving the infinite distance problem"), + T("feature.inverse_square_lighting.key_feature_3", "Does not modify any existing lighting"), + T("feature.inverse_square_lighting.key_feature_4", "Requires the use of mods with lights enabled for inverse square falloff."), + T("feature.inverse_square_lighting.key_feature_5", "Full integration with Light Placer") } }; } inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/LODBlending.cpp b/src/Features/LODBlending.cpp index c1e0e5b91b..fa733c48e5 100644 --- a/src/Features/LODBlending.cpp +++ b/src/Features/LODBlending.cpp @@ -1,5 +1,9 @@ #include "LODBlending.h" +#include "../I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.lod_blending." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LODBlending::Settings, LODTerrainBrightness, @@ -12,20 +16,21 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void LODBlending::DrawSettings() { - ImGui::SliderFloat("LOD Terrain Brightness", &settings.LODTerrainBrightness, 0.01f, 5.f, "%.2f"); - ImGui::SliderFloat("LOD Object Brightness", &settings.LODObjectBrightness, 0.01f, 5.f, "%.2f"); - ImGui::SliderFloat("LOD Object Snow Brightness", &settings.LODObjectSnowBrightness, 0.01f, 5.f, "%.2f"); - ImGui::SliderFloat("LOD Terrain Gamma", &settings.LODTerrainGamma, 0.1f, 3.f, "%.2f"); - ImGui::SliderFloat("LOD Object Gamma", &settings.LODObjectGamma, 0.1f, 3.f, "%.2f"); - ImGui::SliderFloat("LOD Object Snow Gamma", &settings.LODObjectSnowGamma, 0.1f, 3.f, "%.2f"); - ImGui::Checkbox("Disable Terrain Vertex Colors", (bool*)&settings.DisableTerrainVertexColors); + ImGui::SliderFloat(T(TKEY("lod_terrain_brightness"), "LOD Terrain Brightness"), &settings.LODTerrainBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lod_object_brightness"), "LOD Object Brightness"), &settings.LODObjectBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lod_object_snow_brightness"), "LOD Object Snow Brightness"), &settings.LODObjectSnowBrightness, 0.01f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lod_terrain_gamma"), "LOD Terrain Gamma"), &settings.LODTerrainGamma, 0.1f, 3.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lod_object_gamma"), "LOD Object Gamma"), &settings.LODObjectGamma, 0.1f, 3.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lod_object_snow_gamma"), "LOD Object Snow Gamma"), &settings.LODObjectSnowGamma, 0.1f, 3.f, "%.2f"); + ImGui::Checkbox(T(TKEY("disable_terrain_vertex_colors"), "Disable Terrain Vertex Colors"), (bool*)&settings.DisableTerrainVertexColors); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Disables vertex coloring on nearby terrain. " - "Best combined with terrain LOD generated in xLODGen with Vertex Color Intensity set to 0. "); + ImGui::Text("%s", T(TKEY("disable_terrain_vertex_colors_tooltip"), + "Disables vertex coloring on nearby terrain. Best combined with terrain LOD generated in xLODGen with Vertex Color Intensity set to 0.")); } } +#undef I18N_KEY_PREFIX + void LODBlending::LoadSettings(json& o_json) { settings = o_json; @@ -39,4 +44,4 @@ void LODBlending::SaveSettings(json& o_json) void LODBlending::RestoreDefaultSettings() { settings = {}; -} \ No newline at end of file +} diff --git a/src/Features/LODBlending.h b/src/Features/LODBlending.h index d57bab1c7f..9eb866ad02 100644 --- a/src/Features/LODBlending.h +++ b/src/Features/LODBlending.h @@ -3,20 +3,20 @@ struct LODBlending : Feature { virtual inline std::string GetName() override { return "LOD Blending"; } + virtual std::string GetDisplayName() override { return T("feature.lod_blending.name", "LOD Blending"); } virtual inline std::string GetShortName() override { return "LODBlending"; } virtual inline std::string_view GetShaderDefineName() override { return "LOD_BLENDING"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides seamless visual transitions between Level of Detail (LOD) objects and full-detail objects, eliminating harsh transitions and creating smooth visual continuity.", - { "Smooth LOD object brightness blending", - "Enhanced terrain LOD appearance matching", - "Snow-specific LOD brightness adjustment", - "Optional terrain vertex color modification", - "Seamless transition between detail levels" } - }; - } + return { T("feature.lod_blending.description", "Provides seamless visual transitions between Level of Detail (LOD) objects and full-detail objects, eliminating harsh transitions and creating smooth visual continuity."), + { T("feature.lod_blending.key_feature_1", "Smooth LOD object brightness blending"), + T("feature.lod_blending.key_feature_2", "Enhanced terrain LOD appearance matching"), + T("feature.lod_blending.key_feature_3", "Snow-specific LOD brightness adjustment"), + T("feature.lod_blending.key_feature_4", "Optional terrain vertex color modification"), + T("feature.lod_blending.key_feature_5", "Seamless transition between detail levels") } }; + }; + virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }; struct Settings diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index 5d8da8cc2d..4eddc810d2 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -2,11 +2,14 @@ #include "InverseSquareLighting.h" #include "LinearLighting.h" +#include "I18n/I18n.h" #include "Menu/ThemeManager.h" -#include "Utils/ExternalEmittance.h" #include "Shadercache.h" #include "State.h" #include "Util.h" +#include "Utils/ExternalEmittance.h" + +#define I18N_KEY_PREFIX "feature.light_limit_fix." static constexpr uint CLUSTER_MAX_LIGHTS = 128; static constexpr uint MAX_LIGHTS = 1024; @@ -15,30 +18,30 @@ void LightLimitFix::DrawSettings() { auto shaderCache = globals::shaderCache; - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNodeEx(T(TKEY("statistics"), "Statistics"), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Text(std::format("Clustered Light Count : {}", lightCount).c_str()); ImGui::TreePop(); } /////////////////////////////// - ImGui::SeparatorText("Debug"); + ImGui::SeparatorText(T(TKEY("debug"), "Debug")); - if (ImGui::TreeNode("Light Limit Visualization")) { - ImGui::Checkbox("Enable Lights Visualisation", &settings.EnableLightsVisualisation); + if (ImGui::TreeNode(T(TKEY("light_limit_vis"), "Light Limit Visualization"))) { + ImGui::Checkbox(T(TKEY("enable_lights_vis"), "Enable Lights Visualisation"), &settings.EnableLightsVisualisation); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables visualization of the light limit\n"); + ImGui::Text("%s", T(TKEY("enable_lights_vis_tooltip"), "Enables visualization of the light limit\n")); } { static const char* comboOptions[] = { "Light Limit", "Strict Lights Count", "Clustered Lights Count", "Shadow Mask" }; - ImGui::Combo("Lights Visualisation Mode", (int*)&settings.LightsVisualisationMode, comboOptions, 4); + ImGui::Combo(T(TKEY("lights_vis_mode"), "Lights Visualisation Mode"), (int*)&settings.LightsVisualisationMode, comboOptions, 4); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n" - " - Visualise the number of strict lights.\n" - " - Visualise the number of clustered lights.\n" - " - Visualize the Shadow Mask.\n"); + ImGui::Text("%s", T(TKEY("lights_vis_mode_tooltip"), + " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n" + " - Visualise the number of strict lights.\n" + " - Visualise the number of clustered lights.\n" + " - Visualize the Shadow Mask.\n")); } } currentEnableLightsVisualisation = settings.EnableLightsVisualisation; @@ -59,7 +62,7 @@ void LightLimitFix::DrawOverlay() const float pos = ThemeManager::Constants::OVERLAY_WINDOW_POSITION * Util::GetUIScale(); ImGui::SetNextWindowPos(ImVec2(pos, pos), ImGuiCond_Always); ImGui::Begin("##LLFDebug", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "DEBUG FEATURE - LIGHT LIMIT VISUALISATION ENABLED"); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", T(TKEY("debug_feature_enabled"), "DEBUG FEATURE - LIGHT LIMIT VISUALISATION ENABLED")); ImGui::End(); } @@ -582,3 +585,4 @@ void LightLimitFix::Hooks::BSWaterShader_SetupGeometry::thunk(RE::BSShader* This singleton.BSLightingShader_SetupGeometry_Before(Pass); singleton.BSLightingShader_SetupGeometry_After(Pass); }; +#undef I18N_KEY_PREFIX diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index b4a2dcd3dc..0dc60f4c1d 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -7,22 +7,20 @@ struct LightLimitFix : OverlayFeature { public: virtual inline std::string GetName() override { return "Light Limit Fix"; } + virtual std::string GetDisplayName() override { return T("feature.light_limit_fix.name", "Light Limit Fix"); } virtual inline std::string GetShortName() override { return "LightLimitFix"; } virtual inline std::string_view GetShaderDefineName() override { return "LIGHT_LIMIT_FIX"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes.\n" - "This dramatically improves lighting quality and enables more realistic illumination scenarios.", - { "Removes 4-light limit", - "Unlimited dynamic lights", - "Improved lighting quality", - "Enhanced visual realism", - "Enhanced visual realism" } - }; - } + return { T("feature.light_limit_fix.description", "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes.\nThis dramatically improves lighting quality and enables more realistic illumination scenarios."), + { T("feature.light_limit_fix.key_feature_1", "Removes 4-light limit"), + T("feature.light_limit_fix.key_feature_2", "Unlimited dynamic lights"), + T("feature.light_limit_fix.key_feature_3", "Improved lighting quality"), + T("feature.light_limit_fix.key_feature_4", "Enhanced visual realism"), + T("feature.light_limit_fix.key_feature_5", "Enhanced visual realism") } }; + }; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/LinearLighting.cpp b/src/Features/LinearLighting.cpp index 6b15280c43..ce3ead1a31 100644 --- a/src/Features/LinearLighting.cpp +++ b/src/Features/LinearLighting.cpp @@ -1,7 +1,10 @@ #include "LinearLighting.h" +#include "../I18n/I18n.h" #include "State.h" +#define I18N_KEY_PREFIX "feature.linear_lighting." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( LinearLighting::Settings, enableLinearLighting, @@ -32,47 +35,47 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void LinearLighting::DrawSettings() { - ImGui::Checkbox("Enable Linear Lighting", (bool*)&settings.enableLinearLighting); + ImGui::Checkbox(T(TKEY("enable"), "Enable Linear Lighting"), (bool*)&settings.enableLinearLighting); if (ImGui::BeginTabBar("##LinearLightingTabs", ImGuiTabBarFlags_None)) { - if (ImGui::BeginTabItem("General")) { - ImGui::SeparatorText("Gamma Settings"); - ImGui::SliderFloat("Fog Gamma", &settings.fogGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Fog Transparency Gamma", &settings.fogAlphaGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Sky Gamma", &settings.skyGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Volumetric Lighting Gamma", &settings.vlGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Water Gamma", &settings.waterGamma, 0.1f, 3.0f, "%.2f"); - - ImGui::SeparatorText("Multipliers"); - ImGui::SliderFloat("Directional Light Multiplier", &settings.directionalLightMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Ambient Multiplier", &settings.ambientMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Glowmap Multiplier", &settings.glowmapMult, 0.0f, 10.0f, "%.2f"); + if (ImGui::BeginTabItem(T(TKEY("tab_general"), "General"))) { + ImGui::SeparatorText(T(TKEY("gamma_settings"), "Gamma Settings")); + ImGui::SliderFloat(T(TKEY("fog_gamma"), "Fog Gamma"), &settings.fogGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("fog_transparency_gamma"), "Fog Transparency Gamma"), &settings.fogAlphaGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("sky_gamma"), "Sky Gamma"), &settings.skyGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("vl_gamma"), "Volumetric Lighting Gamma"), &settings.vlGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("water_gamma"), "Water Gamma"), &settings.waterGamma, 0.1f, 3.0f, "%.2f"); + + ImGui::SeparatorText(T(TKEY("multipliers"), "Multipliers")); + ImGui::SliderFloat(T(TKEY("directional_light_multiplier"), "Directional Light Multiplier"), &settings.directionalLightMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("ambient_multiplier"), "Ambient Multiplier"), &settings.ambientMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("glowmap_multiplier"), "Glowmap Multiplier"), &settings.glowmapMult, 0.0f, 10.0f, "%.2f"); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Advanced")) { - ImGui::SeparatorText("Gamma Settings"); - ImGui::SliderFloat("Light Gamma", &settings.lightGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Color Gamma", &settings.colorGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Emissive Color Gamma", &settings.emitColorGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Glowmap Gamma", &settings.glowmapGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Ambient Gamma", &settings.ambientGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Effect Gamma", &settings.effectGamma, 0.1f, 3.0f, "%.2f"); - ImGui::SliderFloat("Effect Transparency Gamma", &settings.effectAlphaGamma, 0.1f, 3.0f, "%.2f"); - - ImGui::SeparatorText("Multipliers"); - ImGui::SliderFloat("Vanilla Diffuse Color Multiplier", &settings.vanillaDiffuseColorMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Emissive Color Multiplier", &settings.emitColorMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Point Light Multiplier", &settings.pointLightMult, 0.0f, 10.0f, "%.2f"); - - if (ImGui::TreeNodeEx("Effects", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Effect Lighting Multiplier", &settings.effectLightingMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Membrane Effects Multiplier", &settings.membraneEffectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Blood Effects Multiplier", &settings.bloodEffectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Projected Effects Multiplier", &settings.projectedEffectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Deferred Effects Multiplier", &settings.deferredEffectMult, 0.0f, 10.0f, "%.2f"); - ImGui::SliderFloat("Other Effects Multiplier", &settings.otherEffectMult, 0.0f, 10.0f, "%.2f"); + if (ImGui::BeginTabItem(T(TKEY("tab_advanced"), "Advanced"))) { + ImGui::SeparatorText(T(TKEY("gamma_settings"), "Gamma Settings")); + ImGui::SliderFloat(T(TKEY("light_gamma"), "Light Gamma"), &settings.lightGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("color_gamma"), "Color Gamma"), &settings.colorGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("emissive_color_gamma"), "Emissive Color Gamma"), &settings.emitColorGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("glowmap_gamma"), "Glowmap Gamma"), &settings.glowmapGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("ambient_gamma"), "Ambient Gamma"), &settings.ambientGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("effect_gamma"), "Effect Gamma"), &settings.effectGamma, 0.1f, 3.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("effect_transparency_gamma"), "Effect Transparency Gamma"), &settings.effectAlphaGamma, 0.1f, 3.0f, "%.2f"); + + ImGui::SeparatorText(T(TKEY("multipliers"), "Multipliers")); + ImGui::SliderFloat(T(TKEY("vanilla_diffuse_color_multiplier"), "Vanilla Diffuse Color Multiplier"), &settings.vanillaDiffuseColorMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("emissive_color_multiplier"), "Emissive Color Multiplier"), &settings.emitColorMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("point_light_multiplier"), "Point Light Multiplier"), &settings.pointLightMult, 0.0f, 10.0f, "%.2f"); + + if (ImGui::TreeNodeEx(T(TKEY("effects"), "Effects"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("effect_lighting_multiplier"), "Effect Lighting Multiplier"), &settings.effectLightingMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("membrane_effects_multiplier"), "Membrane Effects Multiplier"), &settings.membraneEffectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("blood_effects_multiplier"), "Blood Effects Multiplier"), &settings.bloodEffectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("projected_effects_multiplier"), "Projected Effects Multiplier"), &settings.projectedEffectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("deferred_effects_multiplier"), "Deferred Effects Multiplier"), &settings.deferredEffectMult, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("other_effects_multiplier"), "Other Effects Multiplier"), &settings.otherEffectMult, 0.0f, 10.0f, "%.2f"); ImGui::TreePop(); } @@ -208,3 +211,5 @@ void LinearLighting::BSLightingShader_SetupGeometry(RE::BSRenderPass* a_pass) } } } + +#undef I18N_KEY_PREFIX diff --git a/src/Features/LinearLighting.h b/src/Features/LinearLighting.h index d67c7a6f21..a3e30ade1c 100644 --- a/src/Features/LinearLighting.h +++ b/src/Features/LinearLighting.h @@ -9,17 +9,16 @@ struct LinearLighting : Feature } virtual inline std::string GetName() override { return "Linear Lighting"; } + virtual std::string GetDisplayName() override { return T("feature.linear_lighting.name", "Linear Lighting"); } virtual inline std::string GetShortName() override { return "LinearLighting"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Linear Lighting does internal color space conversion to improve lighting calculation accuracy.", - { "Customizable gamma correction", - "Corrects lighting calculations", - "Makes PBR really work" } - }; - } + return { T("feature.linear_lighting.description", "Linear Lighting does internal color space conversion to improve lighting calculation accuracy."), + { T("feature.linear_lighting.key_feature_1", "Customizable gamma correction"), + T("feature.linear_lighting.key_feature_2", "Corrects lighting calculations"), + T("feature.linear_lighting.key_feature_3", "Makes PBR really work") } }; + }; virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 739e86d8d9..87e1c5a840 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -23,6 +23,7 @@ #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/Upscaling.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Menu/ProfilingRenderer.h" #include "State.h" @@ -30,6 +31,9 @@ #include "Utils/Format.h" #include "Utils/Game.h" #include "Utils/UI.h" + +#define I18N_KEY_PREFIX "feature.perf_overlay." + #include #include @@ -132,32 +136,15 @@ static const std::unordered_map kShaderTypeTool // VIRTUAL OVERRIDES (Feature.h interface) // ============================================================================ -std::pair> PerformanceOverlay::GetFeatureSummary() -{ - std::string description = "Real-time performance monitoring system that displays FPS, frame times, draw calls, VRAM usage, and detailed shader performance analysis."; - - std::vector keyFeatures = { - "Real-time FPS and frame time monitoring with configurable update intervals", - "Interactive draw call analysis with per-shader type performance breakdown", - "VRAM usage monitoring with visual progress bars", - "Frame time graphs for pre and post-frame generation analysis", - "A/B testing support for performance comparison between configurations", - "Color-coded performance metrics with customizable thresholds", - "Movable overlay window with persistent positioning" - }; - - return { description, keyFeatures }; -} - void PerformanceOverlay::DrawSettings() { auto menu = Menu::GetSingleton(); const auto& themeSettings = menu->GetTheme(); const auto& menuSettings = menu->GetSettings(); - ImGui::Checkbox("Show in Overlay", &this->settings.ShowInOverlay); + ImGui::Checkbox(T(TKEY("show_in_overlay"), "Show in Overlay"), &this->settings.ShowInOverlay); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens performance overlay in a separate window that stays open\neven when the main menu is closed. "); - ImGui::Text("Toggle with "); + ImGui::Text("%s", T(TKEY("show_in_overlay_tooltip"), "Opens performance overlay in a separate window that stays open\neven when the main menu is closed. ")); + ImGui::Text("%s", T(TKEY("toggle_with"), "Toggle with ")); ImGui::SameLine(); ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.OverlayToggleKey).c_str()); @@ -167,53 +154,53 @@ void PerformanceOverlay::DrawSettings() ImGui::Indent(); // Display options - ImGui::TextUnformatted("Display Options"); + ImGui::TextUnformatted(T(TKEY("display_options"), "Display Options")); ImGui::Separator(); - ImGui::Checkbox("Show FPS Counter", &this->settings.ShowFPS); - ImGui::Checkbox("Show Draw Calls", &this->settings.ShowDrawCalls); - ImGui::Checkbox("Show VRAM Usage", &this->settings.ShowVRAM); - ImGui::Checkbox("Show CS Render Passes", &this->settings.ShowCSPasses); + ImGui::Checkbox(T(TKEY("show_fps"), "Show FPS Counter"), &this->settings.ShowFPS); + ImGui::Checkbox(T(TKEY("show_draw_calls"), "Show Draw Calls"), &this->settings.ShowDrawCalls); + ImGui::Checkbox(T(TKEY("show_vram"), "Show VRAM Usage"), &this->settings.ShowVRAM); + ImGui::Checkbox(T(TKEY("show_cs_passes"), "Show CS Render Passes"), &this->settings.ShowCSPasses); bool isFrameGenerationActive = globals::features::upscaling.IsFrameGenerationActive(); if (this->settings.ShowFPS && isFrameGenerationActive) { - ImGui::Checkbox("Show Pre-FG Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); + ImGui::Checkbox(T(TKEY("show_pre_fg_graph"), "Show Pre-FG Frametime Graph"), &this->settings.ShowPreFGFrameTimeGraph); - ImGui::Checkbox("Show Post-FG Frametime Graph", &this->settings.ShowPostFGFrameTimeGraph); + ImGui::Checkbox(T(TKEY("show_post_fg_graph"), "Show Post-FG Frametime Graph"), &this->settings.ShowPostFGFrameTimeGraph); if (ImGui::IsItemHovered()) { if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("FSR Frame Generation uses calculated timing data (2x Pre-FG).\nDLSS Frame Generation provides measured timing data."); + ImGui::Text("%s", T(TKEY("post_fg_graph_tooltip"), "FSR Frame Generation uses calculated timing data (2x Pre-FG).\nDLSS Frame Generation provides measured timing data.")); } } } else if (this->settings.ShowFPS) { - ImGui::Checkbox("Show Frametime Graph", &this->settings.ShowPreFGFrameTimeGraph); + ImGui::Checkbox(T(TKEY("show_frametime_graph"), "Show Frametime Graph"), &this->settings.ShowPreFGFrameTimeGraph); } ImGui::Spacing(); ImGui::Spacing(); // Appearance settings - ImGui::TextUnformatted("Appearance"); + ImGui::TextUnformatted(T(TKEY("appearance"), "Appearance")); ImGui::Separator(); - ImGui::SliderFloat("Text Size", &this->settings.TextSize, 0.8f, 1.2f, "%.2f"); - ImGui::SliderFloat("Background Opacity", &this->settings.BackgroundOpacity, 0.0f, 1.0f, "%.2f"); - ImGui::Checkbox("Show Border", &this->settings.ShowBorder); - ImGui::SliderFloat("Update Interval", &this->settings.UpdateInterval, 0.001f, PerformanceOverlay::Settings::kMaxUpdateInterval, "%.2f seconds"); - ImGui::SliderInt("Frame History Size", &this->settings.FrameHistorySize, + ImGui::SliderFloat(T(TKEY("text_size"), "Text Size"), &this->settings.TextSize, 0.8f, 1.2f, "%.2f"); + ImGui::SliderFloat(T(TKEY("bg_opacity"), "Background Opacity"), &this->settings.BackgroundOpacity, 0.0f, 1.0f, "%.2f"); + ImGui::Checkbox(T(TKEY("show_border"), "Show Border"), &this->settings.ShowBorder); + ImGui::SliderFloat(T(TKEY("update_interval"), "Update Interval"), &this->settings.UpdateInterval, 0.001f, PerformanceOverlay::Settings::kMaxUpdateInterval, "%.2f seconds"); + ImGui::SliderInt(T(TKEY("frame_history_size"), "Frame History Size"), &this->settings.FrameHistorySize, this->settings.kMinFrameHistorySize, this->settings.kMaxFrameHistorySize); ImGui::Separator(); - ImGui::Text("Position:"); - if (ImGui::Button("Reset Position")) { + ImGui::Text("%s", T(TKEY("position"), "Position:")); + if (ImGui::Button(T(TKEY("reset_position"), "Reset Position"))) { this->settings.PositionSet = false; } ImGui::SameLine(); - if (ImGui::Button("Restore Defaults")) { + if (ImGui::Button(T(TKEY("restore_defaults"), "Restore Defaults"))) { RestoreDefaultSettings(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Restores Performance Overlay settings to defaults, including graphs, appearance, and update intervals."); + ImGui::TextUnformatted(T(TKEY("restore_defaults_tooltip"), "Restores Performance Overlay settings to defaults, including graphs, appearance, and update intervals.")); } ImGui::Unindent(); @@ -357,7 +344,7 @@ void PerformanceOverlay::DrawOverlay() } // Create the window - ImGui::Begin("Performance Overlay", NULL, windowFlags); + ImGui::Begin(T(TKEY("overlay_title"), "Performance Overlay"), NULL, windowFlags); // Remember window position for next frame if (ImGui::IsWindowAppearing()) { @@ -425,7 +412,7 @@ void PerformanceOverlay::DrawFPS() ImGui::TableSetupColumn("##value"); ImGui::TableNextColumn(); - ImGui::Text(this->state.isFrameGenerationActive ? "Raw FPS:" : "FPS:"); + ImGui::Text(this->state.isFrameGenerationActive ? T(TKEY("raw_fps"), "Raw FPS:") : T(TKEY("fps"), "FPS:")); ImGui::TableNextColumn(); // Check if buffer is full for the avg @@ -443,7 +430,7 @@ void PerformanceOverlay::DrawFPS() if (this->state.isFrameGenerationActive) { ImGui::TableNextColumn(); - ImGui::Text("Post-FG FPS:"); + ImGui::Text(T(TKEY("post_fg_fps"), "Post-FG FPS:")); ImGui::TableNextColumn(); ImGui::Text("%.1f (%.2f ms)", this->state.postFGSmoothFps, this->state.postFGSmoothFrameTimeMs); } @@ -497,7 +484,7 @@ void PerformanceOverlay::DrawFPS() if (isFrameGenActive) { // Show note that FSR uses calculated data - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Post-FG: Calculated timing (2x Pre-FG)"); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", T(TKEY("post_fg_calculated"), "Post-FG: Calculated timing (2x Pre-FG)")); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("AMD FSR Frame Generation uses calculated timing data (2x Pre-FG).\nNVIDIA DLSS Frame Generation provides measured timing data."); } @@ -508,7 +495,6 @@ void PerformanceOverlay::DrawFPS() } } - void PerformanceOverlay::DrawVRAM() { auto menu = Menu::GetSingleton(); @@ -527,7 +513,7 @@ void PerformanceOverlay::DrawVRAM() float percent = currentGpuUsage / totalGpuMemory; // Center the VRAM text - ImGui::Text("VRAM Usage:"); + ImGui::Text(T(TKEY("vram_usage"), "VRAM Usage:")); // Use a centered text format for the numeric values std::string vramText = std::format("{:.2f}GB/{:.2f}GB ({:.1f}%)", currentGpuUsage, totalGpuMemory, 100 * percent); @@ -546,7 +532,7 @@ void PerformanceOverlay::DrawVRAM() ImGui::ProgressBar(percent, ImVec2(ImGui::GetWindowWidth() * 0.9f, 0.0f), ""); } else { // Display a fallback message if we couldn't get the VRAM info - ImGui::Text("VRAM Usage: Not available"); + ImGui::Text("%s", T(TKEY("vram_not_available"), "VRAM Usage: Not available")); } } @@ -1334,7 +1320,7 @@ void PerformanceOverlay::DrawDrawCallsTable(const std::vector& main overlay.CaptureTestData(); bool anyTestData = !overlay.testData.empty(); if (anyTestData) { - if (ImGui::Button("Clear Test Data")) { + if (ImGui::Button(T(TKEY("clear_test_data"), "Clear Test Data"))) { clearTestDataRequested = true; } } @@ -2043,3 +2029,4 @@ void PerformanceOverlay::UpdateGraphValues() state.updateTimer = 0.0f; } } +#undef I18N_KEY_PREFIX diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index 580cb74e14..d09ba40818 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -121,12 +121,23 @@ struct PerformanceOverlay : OverlayFeature // VIRTUAL OVERRIDES (Feature.h interface) // ============================================================================ std::string GetName() override { return "Performance Overlay"; } + virtual std::string GetDisplayName() override { return T("feature.performance_overlay.name", "Performance Overlay"); } std::string GetShortName() override { return "PerformanceOverlay"; } virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } virtual bool IsInMenu() const override { return true; } bool IsOverlayVisible() const override { return settings.ShowInOverlay; } - virtual std::pair> GetFeatureSummary() override; + virtual std::pair> GetFeatureSummary() override + { + return { T("feature.performance_overlay.description", "Real-time performance monitoring system that displays FPS, frame times, draw calls, VRAM usage, and detailed shader performance analysis."), + { T("feature.performance_overlay.key_feature_1", "Real-time FPS and frame time monitoring with configurable update intervals"), + T("feature.performance_overlay.key_feature_2", "Interactive draw call analysis with per-shader type performance breakdown"), + T("feature.performance_overlay.key_feature_3", "VRAM usage monitoring with visual progress bars"), + T("feature.performance_overlay.key_feature_4", "Frame time graphs for pre and post-frame generation analysis"), + T("feature.performance_overlay.key_feature_5", "A/B testing support for performance comparison between configurations"), + T("feature.performance_overlay.key_feature_6", "Color-coded performance metrics with customizable thresholds"), + T("feature.performance_overlay.key_feature_7", "Movable overlay window with persistent positioning") } }; + } virtual void DrawSettings() override; virtual void DataLoaded() override; void DrawOverlay() override; diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp index 08661b33b2..a0f8b71507 100644 --- a/src/Features/RenderDoc.cpp +++ b/src/Features/RenderDoc.cpp @@ -5,10 +5,14 @@ #include "Utils/FileSystem.h" #include "Utils/Format.h" // Include additional core headers required by the feature implementation +#include "I18n/I18n.h" #include "Menu.h" #include "Plugin.h" #include "State.h" #include "Utils/UI.h" + +#define I18N_KEY_PREFIX "feature.renderdoc." + #include // Include the real RenderDoc API and Windows headers only in the implementation @@ -133,7 +137,7 @@ void RenderDoc::DrawSettings() // Include enable toggle and annotation forcing logic here bool prevRenderDocCapture = enableRenderDocCapture; - if (ImGui::Checkbox("Enable RenderDoc Capture", &enableRenderDocCapture)) { + if (ImGui::Checkbox(T(TKEY("enable_capture"), "Enable RenderDoc Capture"), &enableRenderDocCapture)) { if (enableRenderDocCapture && !prevRenderDocCapture) { globals::state->useFrameAnnotations = globals::state->frameAnnotations; globals::state->frameAnnotations = true; @@ -144,8 +148,8 @@ void RenderDoc::DrawSettings() } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable RenderDoc frame capture for providing debug captures to the Community Shaders team."); - ImGui::Text("Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled."); + ImGui::Text("%s", T(TKEY("enable_capture_tooltip"), "Enable RenderDoc frame capture for providing debug captures to the Community Shaders team.")); + ImGui::Text("%s", T(TKEY("enable_capture_tooltip2"), "Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled.")); } // The rest of the UI renders only when capture is active @@ -155,12 +159,12 @@ void RenderDoc::DrawSettings() const auto& themeSettings = Menu::GetSingleton()->GetTheme(); if (renderDocCaptureEnabled && !renderDocActive) { - ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, "Requires restart to enable RenderDoc capture."); + ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, "%s", T(TKEY("restart_to_enable"), "Requires restart to enable RenderDoc capture.")); return; } if (!renderDocCaptureEnabled && renderDocActive) { - ImGui::TextColored(themeSettings.StatusPalette.Warning, "Requires restart to disable RenderDoc capture, performance will be severely impacted."); + ImGui::TextColored(themeSettings.StatusPalette.Warning, "%s", T(TKEY("restart_to_disable"), "Requires restart to disable RenderDoc capture, performance will be severely impacted.")); return; } @@ -168,9 +172,9 @@ void RenderDoc::DrawSettings() isSectionVisible = true; // Capture Control Section { - auto captureSection = Util::SectionWrapper("Capture Control", "Manual capture creation and basic controls"); + auto captureSection = Util::SectionWrapper(T(TKEY("capture_control"), "Capture Control"), T(TKEY("capture_control_tooltip"), "Manual capture creation and basic controls")); if (captureSection) { - ImGui::TextColored(themeSettings.StatusPalette.InfoColor, "RenderDoc capture is active."); + ImGui::TextColored(themeSettings.StatusPalette.InfoColor, "%s", T(TKEY("capture_active"), "RenderDoc capture is active.")); ImGui::SameLine(); std::string enabledFeaturesPreview; @@ -187,16 +191,16 @@ void RenderDoc::DrawSettings() // Comments input for next capture static char commentsBuffer[kCommentsBufferSize] = { 0 }; - ImGui::InputTextWithHint("##CaptureComments", "Additional comments for next capture (optional)", commentsBuffer, sizeof(commentsBuffer)); - Util::AddTooltip("Additional comments will be appended to automatic metadata and embedded in the .rdc file"); + ImGui::InputTextWithHint("##CaptureComments", T(TKEY("comments_hint"), "Additional comments for next capture (optional)"), commentsBuffer, sizeof(commentsBuffer)); + Util::AddTooltip(T(TKEY("comments_tooltip"), "Additional comments will be appended to automatic metadata and embedded in the .rdc file")); int captureFrameCountUI = static_cast(GetCaptureFrameCount()); - if (ImGui::SliderInt("Capture Frames", &captureFrameCountUI, static_cast(kMinCaptureFrameCount), static_cast(kMaxCaptureFrameCount), "%d", ImGuiSliderFlags_AlwaysClamp)) { + if (ImGui::SliderInt(T(TKEY("capture_frames"), "Capture Frames"), &captureFrameCountUI, static_cast(kMinCaptureFrameCount), static_cast(kMaxCaptureFrameCount), "%d", ImGuiSliderFlags_AlwaysClamp)) { SetCaptureFrameCount(static_cast(captureFrameCountUI)); } - Util::AddTooltip("Number of consecutive frames to capture. 1 uses a normal RenderDoc capture; higher values use TriggerMultiFrameCapture."); + Util::AddTooltip(T(TKEY("capture_frames_tooltip"), "Number of consecutive frames to capture. 1 uses a normal RenderDoc capture; higher values use TriggerMultiFrameCapture.")); - if (ImGui::Button("Create Capture")) { + if (ImGui::Button(T(TKEY("create_capture"), "Create Capture"))) { // Check available disk space before allowing capture try { if (!HasSufficientDiskSpaceForConfiguredCapture()) { @@ -223,16 +227,16 @@ void RenderDoc::DrawSettings() } if (ImGui::BeginPopup("Not enough disk space##RenderDoc")) { - ImGui::Text("Not enough free disk space to create a capture."); - ImGui::Text("At least {} MB of free space is required.", GetRequiredCaptureSpaceBytes() / (1024 * 1024)); - if (ImGui::Button("OK")) { + ImGui::Text("%s", T(TKEY("not_enough_space"), "Not enough free disk space to create a capture.")); + ImGui::Text(T(TKEY("space_required"), "At least {} MB of free space is required."), GetRequiredCaptureSpaceBytes() / (1024 * 1024)); + if (ImGui::Button(T(TKEY("ok"), "OK"))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::SameLine(); - if (ImGui::Button("Open Capture Directory")) { + if (ImGui::Button(T(TKEY("open_capture_dir"), "Open Capture Directory"))) { // Open the directory where captures are saved try { auto capturesDir = GetCapturesDirectory(); @@ -242,11 +246,11 @@ void RenderDoc::DrawSettings() } } - ImGui::TextDisabled("Capture Directory: %s", GetCapturesDirectory().c_str()); - Util::AddTooltip("Right-click to copy the directory path."); + ImGui::TextDisabled(T(TKEY("capture_dir"), "Capture Directory: %s"), GetCapturesDirectory().c_str()); + Util::AddTooltip(T(TKEY("capture_dir_tooltip"), "Right-click to copy the directory path.")); if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Copy Directory Path")) { + if (ImGui::MenuItem(T(TKEY("copy_dir_path"), "Copy Directory Path"))) { // Copy the captures directory path to clipboard try { auto capturesDir = GetCapturesDirectory(); @@ -263,35 +267,35 @@ void RenderDoc::DrawSettings() // Disk Usage Section { - auto diskSection = Util::SectionWrapper("Disk Usage", "Monitor capture storage usage"); + auto diskSection = Util::SectionWrapper(T(TKEY("disk_usage"), "Disk Usage"), T(TKEY("disk_usage_tooltip"), "Monitor capture storage usage")); if (diskSection) { uint32_t diskUsageMB = CalculateCapturesDiskUsage(); float diskUsageGB = static_cast(diskUsageMB) / 1024.0f; // Use color-coded value display for disk usage Util::ColorCodedValueConfig diskUsageConfig = Util::ColorCodedValueConfig::HighIsBad(0.1f, 1.0f, 5.0f); - diskUsageConfig.tooltipText = "Total size of all capture files in the captures directory"; + diskUsageConfig.tooltipText = T(TKEY("capture_size_tooltip"), "Total size of all capture files in the captures directory"); - Util::DrawColorCodedValue("Capture Size", diskUsageGB, std::format("{:.2f} GB", diskUsageGB), diskUsageConfig); + Util::DrawColorCodedValue(T(TKEY("capture_size"), "Capture Size"), diskUsageGB, std::format("{:.2f} GB", diskUsageGB), diskUsageConfig); if (diskUsageMB > 0) { ImGui::SameLine(); - if (ImGui::Button("Clear All Captures")) { + if (ImGui::Button(T(TKEY("clear_all_captures"), "Clear All Captures"))) { ImGui::OpenPopup("Confirm Clear Captures##RenderDoc"); } } if (ImGui::BeginPopup("Confirm Clear Captures##RenderDoc")) { - ImGui::Text("Are you sure you want to delete all capture files?"); - ImGui::Text("This will permanently remove %u MB of capture data.", diskUsageMB); + ImGui::Text("%s", T(TKEY("confirm_delete"), "Are you sure you want to delete all capture files?")); + ImGui::Text(T(TKEY("delete_size"), "This will permanently remove %u MB of capture data."), diskUsageMB); ImGui::Separator(); - if (ImGui::Button("Yes, Delete All")) { + if (ImGui::Button(T(TKEY("yes_delete"), "Yes, Delete All"))) { ClearFrameCaptures(); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) { + if (ImGui::Button(T(TKEY("cancel"), "Cancel"))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); @@ -301,13 +305,13 @@ void RenderDoc::DrawSettings() // Capture Files Section { - auto filesSection = Util::SectionWrapper("Capture Files", "View and manage individual capture files"); + auto filesSection = Util::SectionWrapper(T(TKEY("capture_files"), "Capture Files"), T(TKEY("capture_files_tooltip"), "View and manage individual capture files")); if (filesSection) { // Get cached capture files (auto-refreshes every 5 seconds) const auto& captureFiles = GetCachedCaptureFiles(); // Refresh button - if (ImGui::Button("Refresh List")) { + if (ImGui::Button(T(TKEY("refresh_list"), "Refresh List"))) { ClearFailedDeletions(); RefreshCaptureFileCache(); } @@ -316,14 +320,14 @@ void RenderDoc::DrawSettings() ImGui::TextDisabled("(%zu files)", captureFiles.size()); if (captureFiles.empty()) { - ImGui::TextDisabled("No capture files found."); + ImGui::TextDisabled("%s", T(TKEY("no_files"), "No capture files found.")); } else { // Display custom table with double-click and hover support if (ImGui::BeginTable("##RenderDocCaptures", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortTristate)) { // Setup headers - ImGui::TableSetupColumn("Filename", ImGuiTableColumnFlags_DefaultSort); - ImGui::TableSetupColumn("Size"); - ImGui::TableSetupColumn("Created", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending); + ImGui::TableSetupColumn(T(TKEY("col_filename"), "Filename"), ImGuiTableColumnFlags_DefaultSort); + ImGui::TableSetupColumn(T(TKEY("col_size"), "Size")); + ImGui::TableSetupColumn(T(TKEY("col_created"), "Created"), ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending); ImGui::TableHeadersRow(); // Create a sorted copy of the capture files for display @@ -437,8 +441,8 @@ void RenderDoc::DrawSettings() ImGui::EndTable(); } - ImGui::TextDisabled("Double-click a filename to open the capture file"); - ImGui::TextDisabled("Hover over filenames for file details"); + ImGui::TextDisabled("%s", T(TKEY("double_click_hint"), "Double-click a filename to open the capture file")); + ImGui::TextDisabled("%s", T(TKEY("hover_hint"), "Hover over filenames for file details")); } } } @@ -955,3 +959,4 @@ void RenderDoc::ClearFailedDeletions() failedDeletions.clear(); logger::info("[RenderDoc] Cleared failed deletion tracking"); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index ed3c133392..a5b093f2f0 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -50,13 +50,17 @@ class RenderDoc : public Feature // Feature overrides std::string GetName() override { return "RenderDoc"; } + virtual std::string GetDisplayName() override { return T("feature.render_doc.name", "RenderDoc"); } std::string GetShortName() override { return "RenderDoc"; } std::string_view GetCategory() const override { return FeatureCategories::kUtility; } bool IsCore() const override { return true; } bool IsInMenu() const override { return true; } std::pair> GetFeatureSummary() override { - return { "In-application RenderDoc capture support and convenience UI.", { "Attach comments to captures that appear in RenderDoc UI", "Open captures folder", "Capture file management" } }; + return { T("feature.render_doc.description", "In-application RenderDoc capture support and convenience UI."), + { T("feature.render_doc.key_feature_1", "Attach comments to captures that appear in RenderDoc UI"), + T("feature.render_doc.key_feature_2", "Open captures folder"), + T("feature.render_doc.key_feature_3", "Capture file management") } }; } bool SupportsVR() override { return true; } std::string_view GetShaderDefineName() override { return ""; } diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index f40020466f..cce48d379a 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -2,10 +2,13 @@ #include +#include "../I18n/I18n.h" #include "Deferred.h" #include "State.h" #include "Util.h" +#define I18N_KEY_PREFIX "feature.ssgi." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ScreenSpaceGI::Settings, Enabled, @@ -45,48 +48,48 @@ void ScreenSpaceGI::DrawSettings() static bool showAdvanced; if (!ShadersOK()) - ImGui::TextColored({ 1, 0, 0, 1 }, "Compute shaders failed to compile!"); + ImGui::TextColored({ 1, 0, 0, 1 }, "%s", T(TKEY("shader_compile_error"), "Compute shaders failed to compile!")); /////////////////////////////// - ImGui::SeparatorText("Toggles"); + ImGui::SeparatorText(T(TKEY("toggles"), "Toggles")); - ImGui::Checkbox("Show Advanced Options", &showAdvanced); + ImGui::Checkbox(T(TKEY("show_advanced"), "Show Advanced Options"), &showAdvanced); if (ImGui::BeginTable("Toggles", 4)) { ImGui::TableNextColumn(); - ImGui::Checkbox("Enabled", &settings.Enabled); + ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), &settings.Enabled); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable Screen Space Global Illumination. When disabled, all other settings are ignored."); + ImGui::Text("%s", T(TKEY("enabled_tooltip"), "Enable Screen Space Global Illumination. When disabled, all other settings are ignored.")); } ImGui::TableNextColumn(); { auto ilToggleGuard = Util::DisableGuard(!settings.Enabled); - recompileFlag |= ImGui::Checkbox("Indirect Lighting (IL)", &settings.EnableGI); + recompileFlag |= ImGui::Checkbox(T(TKEY("indirect_lighting"), "Indirect Lighting (IL)"), &settings.EnableGI); } ImGui::TableNextColumn(); { auto vanillaSSAOGuard = Util::DisableGuard(globals::game::isVR); - ImGui::Checkbox("Vanilla SSAO", &settings.EnableVanillaSSAO); + ImGui::Checkbox(T(TKEY("vanilla_ssao"), "Vanilla SSAO"), &settings.EnableVanillaSSAO); if (auto _tt = Util::HoverTooltipWrapper()) { if (globals::game::isVR) - ImGui::Text("Vanilla SSAO is not supported in VR."); + ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip_vr"), "Vanilla SSAO is not supported in VR.")); else - ImGui::Text("Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening."); + ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip"), "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.")); } } ImGui::TableNextColumn(); if (showAdvanced) { - recompileFlag |= ImGui::Checkbox("(Experimental) HQ Specular IL", &settings.EnableExperimentalSpecularGI); + recompileFlag |= ImGui::Checkbox(T(TKEY("hq_specular_il"), "(Experimental) HQ Specular IL"), &settings.EnableExperimentalSpecularGI); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("An experimental specular GI that is more accurate but requires more samples. Won't be blurred."); + ImGui::Text("%s", T(TKEY("hq_specular_il_tooltip"), "An experimental specular GI that is more accurate but requires more samples. Won't be blurred.")); } ImGui::EndTable(); } /////////////////////////////// - ImGui::SeparatorText("Quality/Performance"); + ImGui::SeparatorText(T(TKEY("quality_performance"), "Quality/Performance")); { auto qualityGuard = Util::DisableGuard(!settings.Enabled); @@ -95,7 +98,7 @@ void ScreenSpaceGI::DrawSettings() auto select = [](auto flatVal, auto vrVal) { return globals::game::isVR ? vrVal : flatVal; }; ImGui::TableNextColumn(); - if (ImGui::Button("AO only", { -1, 0 })) { + if (ImGui::Button(T(TKEY("ao_only"), "AO only"), { -1, 0 })) { settings.NumSlices = select(1, 3); settings.NumSteps = select(6, 8); settings.EnableBlur = true; @@ -107,7 +110,7 @@ void ScreenSpaceGI::DrawSettings() } ImGui::TableNextColumn(); - if (ImGui::Button("Low", { -1, 0 })) { + if (ImGui::Button(T(TKEY("low"), "Low"), { -1, 0 })) { settings.NumSlices = 10; settings.NumSteps = 12; settings.ResolutionMode = 2; @@ -116,10 +119,10 @@ void ScreenSpaceGI::DrawSettings() recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Quarter res and blurry."); + ImGui::Text("%s", T(TKEY("low_tooltip"), "Quarter res and blurry.")); ImGui::TableNextColumn(); - if (ImGui::Button("Standard", { -1, 0 })) { + if (ImGui::Button(T(TKEY("standard"), "Standard"), { -1, 0 })) { settings.NumSlices = 4; settings.NumSteps = 8; settings.ResolutionMode = 1; @@ -128,10 +131,10 @@ void ScreenSpaceGI::DrawSettings() recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Half res and somewhat stable."); + ImGui::Text("%s", T(TKEY("standard_tooltip"), "Half res and somewhat stable.")); ImGui::TableNextColumn(); - if (ImGui::Button("Extreme", { -1, 0 })) { + if (ImGui::Button(T(TKEY("extreme"), "Extreme"), { -1, 0 })) { settings.NumSlices = 4; settings.NumSteps = 8; settings.ResolutionMode = 0; @@ -140,10 +143,10 @@ void ScreenSpaceGI::DrawSettings() recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Full res and clean."); + ImGui::Text("%s", T(TKEY("extreme_tooltip"), "Full res and clean.")); ImGui::TableNextColumn(); - if (ImGui::Button("Reference", { -1, 0 })) { + if (ImGui::Button(T(TKEY("reference"), "Reference"), { -1, 0 })) { settings.NumSlices = 8; settings.NumSteps = 10; settings.ResolutionMode = 0; @@ -152,56 +155,56 @@ void ScreenSpaceGI::DrawSettings() recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Reference mode."); + ImGui::Text("%s", T(TKEY("reference_tooltip"), "Reference mode.")); ImGui::EndTable(); } if (showAdvanced) { - ImGui::SliderInt("Slices", (int*)&settings.NumSlices, 1, 10); + ImGui::SliderInt(T(TKEY("slices"), "Slices"), (int*)&settings.NumSlices, 1, 10); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "How many directions do the samples take.\n" - "Controls noise."); + ImGui::Text("%s", T(TKEY("slices_tooltip"), + "How many directions do the samples take.\n" + "Controls noise.")); - ImGui::SliderInt("Steps Per Slice", (int*)&settings.NumSteps, 1, 20); + ImGui::SliderInt(T(TKEY("steps_per_slice"), "Steps Per Slice"), (int*)&settings.NumSteps, 1, 20); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "How many samples does it take in one direction.\n" - "Controls accuracy of lighting, and noise when effect radius is large."); + ImGui::Text("%s", T(TKEY("steps_per_slice_tooltip"), + "How many samples does it take in one direction.\n" + "Controls accuracy of lighting, and noise when effect radius is large.")); } if (ImGui::BeginTable("Less Work", 3)) { ImGui::TableNextColumn(); - recompileFlag |= ImGui::RadioButton("Full Res", &settings.ResolutionMode, 0); + recompileFlag |= ImGui::RadioButton(T(TKEY("full_res"), "Full Res"), &settings.ResolutionMode, 0); ImGui::TableNextColumn(); - recompileFlag |= ImGui::RadioButton("Half Res", &settings.ResolutionMode, 1); + recompileFlag |= ImGui::RadioButton(T(TKEY("half_res"), "Half Res"), &settings.ResolutionMode, 1); ImGui::TableNextColumn(); - recompileFlag |= ImGui::RadioButton("Quarter Res", &settings.ResolutionMode, 2); + recompileFlag |= ImGui::RadioButton(T(TKEY("quarter_res"), "Quarter Res"), &settings.ResolutionMode, 2); ImGui::EndTable(); } } /////////////////////////////// - ImGui::SeparatorText("Visual"); + ImGui::SeparatorText(T(TKEY("visual"), "Visual")); { auto visualGuard = Util::DisableGuard(!settings.Enabled); - ImGui::SliderFloat("AO Power", &settings.AOPower, 0.f, 6.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("ao_power"), "AO Power"), &settings.AOPower, 0.f, 6.f, "%.2f"); { auto ilGuard = Util::DisableGuard(!settings.EnableGI); - ImGui::SliderFloat("IL Source Brightness", &settings.GIStrength, 0.f, 6.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("il_source_brightness"), "IL Source Brightness"), &settings.GIStrength, 0.f, 6.f, "%.2f"); } ImGui::Separator(); - ImGui::SliderFloat("AO radius", &settings.AORadius, 10.f, 1024.0f, "%.1f units"); + ImGui::SliderFloat(T(TKEY("ao_radius"), "AO radius"), &settings.AORadius, 10.f, 1024.0f, "%.1f units"); if (auto _tt = Util::HoverTooltipWrapper()) { std::vector tooltipLines = { - "A smaller radius produces tighter AO.", + T(TKEY("ao_radius_tooltip"), "A smaller radius produces tighter AO."), Util::Units::FormatDistance(settings.AORadius) }; Util::DrawMultiLineTooltip(tooltipLines); @@ -210,10 +213,10 @@ void ScreenSpaceGI::DrawSettings() { auto ilRadiusGuard = Util::DisableGuard(!settings.EnableGI); - ImGui::SliderFloat("IL radius", &settings.GIRadius, 10.f, 1024.0f, "%.1f units"); + ImGui::SliderFloat(T(TKEY("il_radius"), "IL radius"), &settings.GIRadius, 10.f, 1024.0f, "%.1f units"); if (auto _tt = Util::HoverTooltipWrapper()) { std::vector tooltipLines = { - "A larger radius produces wider IL.", + T(TKEY("il_radius_tooltip"), "A larger radius produces wider IL."), Util::Units::FormatDistance(settings.GIRadius) }; Util::DrawMultiLineTooltip(tooltipLines); @@ -221,16 +224,16 @@ void ScreenSpaceGI::DrawSettings() } if (showAdvanced) { - ImGui::SliderFloat("Min Screen Radius", &settings.MinScreenRadius, 0.f, 0.05f, "%.3f"); + ImGui::SliderFloat(T(TKEY("min_screen_radius"), "Min Screen Radius"), &settings.MinScreenRadius, 0.f, 0.05f, "%.3f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "The minimum screen-space effect radius as proportion of display width, to prevent far field AO being too small."); + ImGui::Text("%s", T(TKEY("min_screen_radius_tooltip"), + "The minimum screen-space effect radius as proportion of display width, to prevent far field AO being too small.")); } - ImGui::SliderFloat2("Depth Fade Range", &settings.DepthFadeRange.x, 1e4, 5e4, "%.0f units"); + ImGui::SliderFloat2(T(TKEY("depth_fade_range"), "Depth Fade Range"), &settings.DepthFadeRange.x, 1e4, 5e4, "%.0f units"); if (auto _tt = Util::HoverTooltipWrapper()) { std::vector tooltipLines = { - "Distance range where depth-based effects fade out.", + T(TKEY("depth_fade_range_tooltip"), "Distance range where depth-based effects fade out."), "Near: " + Util::Units::FormatDistance(settings.DepthFadeRange.x), "Far: " + Util::Units::FormatDistance(settings.DepthFadeRange.y) }; @@ -240,10 +243,10 @@ void ScreenSpaceGI::DrawSettings() if (showAdvanced) { ImGui::Separator(); - ImGui::SliderFloat("Thickness", &settings.Thickness, 0.f, 128.0f, "%.1f units"); + ImGui::SliderFloat(T(TKEY("thickness"), "Thickness"), &settings.Thickness, 0.f, 128.0f, "%.1f units"); if (auto _tt = Util::HoverTooltipWrapper()) { std::vector tooltipLines = { - "How thick the occluders are. Only affects AO.", + T(TKEY("thickness_tooltip"), "How thick the occluders are. Only affects AO."), Util::Units::FormatDistance(settings.Thickness) }; Util::DrawMultiLineTooltip(tooltipLines); @@ -252,34 +255,34 @@ void ScreenSpaceGI::DrawSettings() } /////////////////////////////// - ImGui::SeparatorText("Visual - IL"); + ImGui::SeparatorText(T(TKEY("visual_il"), "Visual - IL")); { auto visualILGuard = Util::DisableGuard(!settings.Enabled || !settings.EnableGI); if (showAdvanced) { - ImGui::SliderFloat("IL Distance Compensation", &settings.GIDistanceCompensation, -5.0f, 5.0f, "%.1f"); + ImGui::SliderFloat(T(TKEY("il_distance_compensation"), "IL Distance Compensation"), &settings.GIDistanceCompensation, -5.0f, 5.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Brighten/Dimming further radiance samples."); + ImGui::Text("%s", T(TKEY("il_distance_compensation_tooltip"), "Brighten/Dimming further radiance samples.")); ImGui::Separator(); } - Util::PercentageSlider("IL Saturation", &settings.GISaturation); + Util::PercentageSlider(T(TKEY("il_saturation"), "IL Saturation"), &settings.GISaturation); } /////////////////////////////// - ImGui::SeparatorText("Denoising"); + ImGui::SeparatorText(T(TKEY("denoising"), "Denoising")); { auto denoiseGuard = Util::DisableGuard(!settings.Enabled); if (ImGui::BeginTable("denoisers", 2)) { ImGui::TableNextColumn(); - recompileFlag |= ImGui::Checkbox("Temporal Denoiser", &settings.EnableTemporalDenoiser); + recompileFlag |= ImGui::Checkbox(T(TKEY("temporal_denoiser"), "Temporal Denoiser"), &settings.EnableTemporalDenoiser); ImGui::TableNextColumn(); - ImGui::Checkbox("Blur", &settings.EnableBlur); + ImGui::Checkbox(T(TKEY("blur"), "Blur"), &settings.EnableBlur); ImGui::EndTable(); } @@ -289,9 +292,9 @@ void ScreenSpaceGI::DrawSettings() { auto temporalGuard = Util::DisableGuard(!settings.EnableTemporalDenoiser); - ImGui::SliderInt("Max Frame Accumulation", (int*)&settings.MaxAccumFrames, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderInt(T(TKEY("max_frame_accumulation"), "Max Frame Accumulation"), (int*)&settings.MaxAccumFrames, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("How many past frames to accumulate results with. Higher values are less noisy but potentially cause ghosting."); + ImGui::Text("%s", T(TKEY("max_frame_accumulation_tooltip"), "How many past frames to accumulate results with. Higher values are less noisy but potentially cause ghosting.")); } ImGui::Separator(); @@ -299,35 +302,35 @@ void ScreenSpaceGI::DrawSettings() { auto disocclusionGuard = Util::DisableGuard(!settings.EnableTemporalDenoiser && !settings.EnableGI); - Util::PercentageSlider("Movement Disocclusion", &settings.DepthDisocclusion, 0.f, 20.f); + Util::PercentageSlider(T(TKEY("movement_disocclusion"), "Movement Disocclusion"), &settings.DepthDisocclusion, 0.f, 20.f); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "If a pixel has moved too far from the last frame, its radiance will not be carried to this frame.\n" - "Lower values are stricter."); + ImGui::Text("%s", T(TKEY("movement_disocclusion_tooltip"), + "If a pixel has moved too far from the last frame, its radiance will not be carried to this frame.\n" + "Lower values are stricter.")); ImGui::Separator(); } { auto blurGuard = Util::DisableGuard(!settings.EnableBlur); - ImGui::SliderFloat("Blur Radius", &settings.BlurRadius, 0.f, 30.f, "%.1f px"); + ImGui::SliderFloat(T(TKEY("blur_radius"), "Blur Radius"), &settings.BlurRadius, 0.f, 30.f, "%.1f px"); if (showAdvanced) { - ImGui::SliderFloat("Geometry Weight", &settings.DistanceNormalisation, 0.f, 5.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("geometry_weight"), "Geometry Weight"), &settings.DistanceNormalisation, 0.f, 5.f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "Higher value makes the blur more sensitive to differences in geometry."); + ImGui::Text("%s", T(TKEY("geometry_weight_tooltip"), + "Higher value makes the blur more sensitive to differences in geometry.")); } } } } /////////////////////////////// - ImGui::SeparatorText("Debug"); + ImGui::SeparatorText(T(TKEY("debug"), "Debug")); - if (ImGui::TreeNode("Buffer Viewer")) { + if (ImGui::TreeNode(T(TKEY("buffer_viewer"), "Buffer Viewer"))) { static float debugRescale = .3f; - ImGui::SliderFloat("View Resize", &debugRescale, 0.f, 1.f); + ImGui::SliderFloat(T(TKEY("view_resize"), "View Resize"), &debugRescale, 0.f, 1.f); BUFFER_VIEWER_NODE(texNoise, debugRescale) BUFFER_VIEWER_NODE(texWorkingDepth, debugRescale) @@ -342,7 +345,6 @@ void ScreenSpaceGI::DrawSettings() ImGui::TreePop(); } - } void ScreenSpaceGI::LoadSettings(json& o_json) @@ -994,3 +996,5 @@ void ScreenSpaceGI::DrawSSGI() context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); context->CSSetShader(nullptr, nullptr, 0); } + +#undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index 45c0c1a03e..e2dddc30de 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -11,30 +11,23 @@ struct ScreenSpaceGI : Feature bool inline SupportsVR() override { return true; } virtual inline std::string GetName() override { return "Screen Space GI"; } + virtual std::string GetDisplayName() override { return T("feature.screen_space_gi.name", "Screen Space GI"); } virtual inline std::string GetShortName() override { return "ScreenSpaceGI"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - std::string desc = - "Screen Space Global Illumination adds realistic indirect lighting and " - "ambient occlusion to the game. This technique simulates how light " - "bounces off surfaces to illuminate other objects naturally."; + std::string desc = T("feature.screen_space_gi.description", "Screen Space Global Illumination adds realistic indirect lighting and ambient occlusion to the game. This technique simulates how light bounces off surfaces to illuminate other objects naturally."); if (REL::Module::IsVR()) { - desc += - "\n\nWarning: In VR, this feature may have visual artifacts and " - "can have a significant performance impact due to the nature of " - "screen space effects."; + desc += T("feature.screen_space_gi.vr_warning", "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects."); } - return std::make_pair( - desc, - std::vector{ - "Realistic indirect lighting", - "Enhanced ambient occlusion", - "Improved visual depth and atmosphere", - "Temporal denoising for smooth results", - "Configurable quality and performance settings" }); + return { desc, + { T("feature.screen_space_gi.key_feature_1", "Realistic indirect lighting"), + T("feature.screen_space_gi.key_feature_2", "Enhanced ambient occlusion"), + T("feature.screen_space_gi.key_feature_3", "Improved visual depth and atmosphere"), + T("feature.screen_space_gi.key_feature_4", "Temporal denoising for smooth results"), + T("feature.screen_space_gi.key_feature_5", "Configurable quality and performance settings") } }; } virtual void RestoreDefaultSettings() override; diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index ffc7cce408..c5452d91fe 100644 --- a/src/Features/ScreenSpaceShadows.cpp +++ b/src/Features/ScreenSpaceShadows.cpp @@ -1,9 +1,12 @@ #include "ScreenSpaceShadows.h" #include "Features/TerrainBlending.h" +#include "I18n/I18n.h" #include "State.h" #include "Utils/D3D.h" +#define I18N_KEY_PREFIX "feature.screen_space_shadows." + #pragma warning(push) #pragma warning(disable: 4838 4244) #include "ScreenSpaceShadows/bend_sss_cpu.h" @@ -21,34 +24,34 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void ScreenSpaceShadows::DrawSettings() { - if (ImGui::TreeNodeEx("General", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable", (bool*)&bendSettings.Enable); + if (ImGui::TreeNodeEx(T(TKEY("general"), "General"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable"), "Enable"), (bool*)&bendSettings.Enable); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Enable screen-space contact shadows from the sun/moon direction."); + ImGui::Text("%s", T(TKEY("enable_tooltip"), "Enable screen-space contact shadows from the sun/moon direction.")); - ImGui::SliderInt("Sample Count Multiplier", (int*)&bendSettings.SampleCount, 1, 4); + ImGui::SliderInt(T(TKEY("sample_count"), "Sample Count Multiplier"), (int*)&bendSettings.SampleCount, 1, 4); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Multiplier for shadow ray sample count. Higher values increase shadow reach at the cost of performance. Adapts to render resolution."); + ImGui::Text("%s", T(TKEY("sample_count_tooltip"), "Multiplier for shadow ray sample count. Higher values increase shadow reach at the cost of performance. Adapts to render resolution.")); - ImGui::SliderFloat("Surface Thickness", &bendSettings.SurfaceThickness, 0.005f, 0.05f); + ImGui::SliderFloat(T(TKEY("surface_thickness"), "Surface Thickness"), &bendSettings.SurfaceThickness, 0.005f, 0.05f); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Assumed thickness of surfaces for shadow detection. Lower values produce thinner, more precise shadows."); + ImGui::Text("%s", T(TKEY("surface_thickness_tooltip"), "Assumed thickness of surfaces for shadow detection. Lower values produce thinner, more precise shadows.")); - ImGui::SliderFloat("Bilinear Threshold", &bendSettings.BilinearThreshold, 0.02f, 1.0f); + ImGui::SliderFloat(T(TKEY("bilinear_threshold"), "Bilinear Threshold"), &bendSettings.BilinearThreshold, 0.02f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges."); + ImGui::Text("%s", T(TKEY("bilinear_threshold_tooltip"), "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.")); - ImGui::SliderFloat("Shadow Contrast", &bendSettings.ShadowContrast, 0.0f, 4.0f); + ImGui::SliderFloat(T(TKEY("shadow_contrast"), "Shadow Contrast"), &bendSettings.ShadowContrast, 0.0f, 4.0f); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Contrast boost for the shadow transition. Higher values produce harder shadow edges."); + ImGui::Text("%s", T(TKEY("shadow_contrast_tooltip"), "Contrast boost for the shadow transition. Higher values produce harder shadow edges.")); if (globals::game::isVR && globals::state->IsDeveloperMode()) { - ImGui::Checkbox("VR Stereo Sync", &enableStereoSync); + ImGui::Checkbox(T(TKEY("vr_stereo_sync"), "VR Stereo Sync"), &enableStereoSync); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text( - "Synchronizes shadow data between left and right eyes via bilateral reprojection " - "and applies a depth-weighted blur to reduce per-eye noise. " - "Uses min-blend so if either eye detects an occluder, the shadow is preserved. "); + ImGui::Text("%s", T(TKEY("vr_stereo_sync_tooltip"), + "Synchronizes shadow data between left and right eyes via bilateral reprojection " + "and applies a depth-weighted blur to reduce per-eye noise. " + "Uses min-blend so if either eye detects an occluder, the shadow is preserved. ")); } ImGui::Spacing(); @@ -445,3 +448,4 @@ void ScreenSpaceShadows::SetupResources() } } } +#undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenSpaceShadows.h b/src/Features/ScreenSpaceShadows.h index d9efa27648..48c79556c1 100644 --- a/src/Features/ScreenSpaceShadows.h +++ b/src/Features/ScreenSpaceShadows.h @@ -6,21 +6,19 @@ struct ScreenSpaceShadows : Feature { public: virtual inline std::string GetName() override { return "Screen Space Shadows"; } + virtual std::string GetDisplayName() override { return T("feature.screen_space_shadows.name", "Screen Space Shadows"); } virtual inline std::string GetShortName() override { return "ScreenSpaceShadows"; } virtual inline std::string_view GetShaderDefineName() override { return "SCREEN_SPACE_SHADOWS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\n" - "This technique adds fine-detail shadows that traditional shadow mapping might miss.", - { "Enhanced contact shadows", - "Improved shadow detail", - "Better shadow accuracy", - "Fine-scale shadow effects", - "Configurable shadow contrast" } - }; + return { T("feature.screen_space_shadows.description", "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\nThis technique adds fine-detail shadows that traditional shadow mapping might miss."), + { T("feature.screen_space_shadows.key_feature_1", "Enhanced contact shadows"), + T("feature.screen_space_shadows.key_feature_2", "Improved shadow detail"), + T("feature.screen_space_shadows.key_feature_3", "Better shadow accuracy"), + T("feature.screen_space_shadows.key_feature_4", "Fine-scale shadow effects"), + T("feature.screen_space_shadows.key_feature_5", "Configurable shadow contrast") } }; } bool HasShaderDefine(RE::BSShader::Type shaderType) override; diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index a9d3c4d347..39f7aaf5bb 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -9,9 +9,13 @@ #include "Features/HDRDisplay.h" #include "Features/Upscaling.h" +#include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Utils/FileSystem.h" +#define I18N_KEY_PREFIX "feature.screenshot." + #include #include @@ -528,8 +532,8 @@ namespace } const GUID& codec = saveAsPng ? - DirectX::GetWICCodec(DirectX::WIC_CODEC_PNG) : - DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP); + DirectX::GetWICCodec(DirectX::WIC_CODEC_PNG) : + DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP); return SUCCEEDED(DirectX::SaveToWICFile( *saveImage, DirectX::WIC_FLAGS_NONE, @@ -606,17 +610,18 @@ void ScreenshotFeature::SaveSettings(json& a_json) void ScreenshotFeature::DrawSettings() { - ImGui::TextWrapped("Capture and save run asynchronously without stalling the game."); + ImGui::TextWrapped("%s", T(TKEY("async_note"), "Capture and save run asynchronously without stalling the game.")); const bool hdrCaptureAvailable = globals::features::hdrDisplay.loaded && globals::features::hdrDisplay.settings.enableHDR; if (hdrCaptureAvailable) { - ImGui::TextWrapped( - "HDR enabled: saves the displayed frame as PNG with HDR10 metadata (48 bpp RGB, cICP/cLLi). " - "Use an HDR-aware viewer such as Windows Photos (HDR on) or Special K SKIF."); + ImGui::TextWrapped("%s", + T(TKEY("hdr_note"), + "HDR enabled: saves the displayed frame as PNG with HDR10 metadata (48 bpp RGB, cICP/cLLi). " + "Use an HDR-aware viewer such as Windows Photos (HDR on) or Special K SKIF.")); ImGui::SliderInt( - "HDR PNG bit depth", + T(TKEY("hdr_bit_depth"), "HDR PNG bit depth"), reinterpret_cast(&hdrPngBitDepth), 7, 16, @@ -624,22 +629,24 @@ void ScreenshotFeature::DrawSettings() ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text( - "Quantization for the 48 bpp RGB PNG payload. 11-bit is a good default; " - "higher values increase file size with diminishing returns."); + "%s", T(TKEY("hdr_bit_depth_tooltip"), + "Quantization for the 48 bpp RGB PNG payload. 11-bit is a good default; " + "higher values increase file size with diminishing returns.")); } else { - ImGui::TextWrapped( - "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. " - "SDR and VR captures use the lossless format selected below."); + ImGui::TextWrapped("%s", + T(TKEY("sdr_note"), + "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. " + "SDR and VR captures use the lossless format selected below.")); } - if (ImGui::Button("Take Screenshot Now")) { + if (ImGui::Button(T(TKEY("take_screenshot"), "Take Screenshot Now"))) { captureRequested = true; } ImGui::SameLine(); - ImGui::Checkbox("Apply crop", &applyCropToScreenshot); + ImGui::Checkbox(T(TKEY("apply_crop"), "Apply crop"), &applyCropToScreenshot); - ImGui::SeparatorText("Output"); + ImGui::SeparatorText(T(TKEY("output"), "Output")); ImGui::Checkbox("Copy saved file to clipboard", ©ToClipboard); if (auto _tt = Util::HoverTooltipWrapper()) @@ -658,7 +665,7 @@ void ScreenshotFeature::DrawSettings() char buf[260]; strncpy_s(buf, sizeof(buf), screenshotPath.c_str(), _TRUNCATE); - ImGui::PushItemWidth(-FLT_MIN - 120.0f); // leave room for Open button + label + ImGui::PushItemWidth(-FLT_MIN - 120.0f); if (ImGui::InputText("##ScreenshotFolder", buf, sizeof(buf))) { screenshotPath = buf; } @@ -666,33 +673,35 @@ void ScreenshotFeature::DrawSettings() ImGui::SameLine(); const bool canOpen = !screenshotPath.empty(); ImGui::BeginDisabled(!canOpen); - if (ImGui::Button("Open")) { + if (ImGui::Button(T(TKEY("open"), "Open"))) { std::error_code ec; std::filesystem::create_directories(screenshotPath, ec); ShellExecuteA(nullptr, "open", screenshotPath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); } ImGui::EndDisabled(); ImGui::SameLine(); - ImGui::Text("Folder"); + ImGui::Text("%s", T(TKEY("folder"), "Folder")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Relative paths resolve against the Skyrim install dir."); - ImGui::Text("Absolute paths (e.g. D:\\Captures) save there directly."); + ImGui::Text("%s", T(TKEY("folder_tooltip"), + "Relative paths resolve against the Skyrim install dir.\n" + "Absolute paths (e.g. D:\\Captures) save there directly.")); } auto& menuSettings = Menu::GetSingleton()->GetSettings(); Util::InputComboWidget( - "Hotkey", + T(TKEY("hotkey"), "Hotkey"), menuSettings.ScreenshotKey, Menu::GetSingleton()->settingScreenshotKey, "Change##ScreenshotFeature"); if (HotkeyCollidesWithVanilla()) { Util::Text::WrappedWarning( - "This hotkey collides with vanilla PrintScreen; both saves will fire. " - "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a different hotkey above."); + T(TKEY("hotkey_collision"), + "This hotkey collides with vanilla PrintScreen; both saves will fire. " + "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a different hotkey above.")); } - ImGui::SeparatorText("Crop"); + ImGui::SeparatorText(T(TKEY("crop"), "Crop")); // Preview reflects what Capture() would save. Full source frame so VR users // can drag-crop across the eye boundary if a seeded preset doesn't fit. @@ -953,3 +962,4 @@ void ScreenshotFeature::Capture() screenshot.copyToClipboard = copyToClipboard; EnqueueScreenshot(std::move(screenshot)); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index 660ed97ad4..c5a87cb8f7 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -14,6 +14,7 @@ struct ScreenshotFeature : public Feature { virtual ~ScreenshotFeature(); virtual std::string GetName() override { return "Screenshot"; } + virtual std::string GetDisplayName() override { return T("feature.screenshot.name", "Screenshot"); } virtual std::string GetShortName() override { return "Screenshot"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kUtility; } diff --git a/src/Features/Skin.cpp b/src/Features/Skin.cpp index 9bd9bde1a5..3b32da2046 100644 --- a/src/Features/Skin.cpp +++ b/src/Features/Skin.cpp @@ -9,6 +9,7 @@ #include "State.h" #include "DynamicWetness_PublicAPI.h" +#include "I18n/I18n.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Skin::Settings, @@ -42,149 +43,149 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void Skin::DrawSettings() { - ImGui::Checkbox("Enable Advanced Skin", &settings.EnableSkin); + ImGui::Checkbox(T("feature.skin.enable_advanced_skin", "Enable Advanced Skin"), &settings.EnableSkin); - ImGui::Text("Advanced Skin Shader using dual specular lobes."); + ImGui::Text("%s", T("feature.skin.advanced_skin_shader_using_dual_specular_lobes", "Advanced Skin Shader using dual specular lobes.")); ImGui::Spacing(); - ImGui::SliderFloat("Primary Roughness", &settings.SkinMainRoughness, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.primary_roughness", "Primary Roughness"), &settings.SkinMainRoughness, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls microscopic roughness of stratum corneum layer"); + ImGui::Text("%s", T("feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer", "Controls microscopic roughness of stratum corneum layer")); } - ImGui::SliderFloat("Secondary Roughness", &settings.SkinSecondRoughness, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.secondary_roughness", "Secondary Roughness"), &settings.SkinSecondRoughness, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Smoothness of epidermal cell layer reflections"); - ImGui::BulletText("Should be 30-50%% lower than Primary"); + ImGui::Text("%s", T("feature.skin.smoothness_of_epidermal_cell_layer_reflections", "Smoothness of epidermal cell layer reflections")); + ImGui::BulletText(T("feature.skin.should_be_30_50_lower_than_primary", "Should be 30-50%% lower than Primary")); } - ImGui::SliderFloat("Specular Texture Multiplier", &settings.SkinSpecularTexMultiplier, 0.0f, 10.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.specular_texture_multiplier", "Specular Texture Multiplier"), &settings.SkinSpecularTexMultiplier, 0.0f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Multiplier for specular map"); - ImGui::BulletText("A multiplier for the vanilla specular map, applied to the first layer's roughness"); + ImGui::Text("%s", T("feature.skin.multiplier_for_specular_map", "Multiplier for specular map")); + ImGui::BulletText("%s", T("feature.skin.a_multiplier_for_the_vanilla_specular_map_applied", "A multiplier for the vanilla specular map, applied to the first layer's roughness")); } - ImGui::SliderFloat("Secondary Specular Strength", &settings.SecondarySpecularStrength, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.secondary_specular_strength", "Secondary Specular Strength"), &settings.SecondarySpecularStrength, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Intensity of secondary specular highlights"); + ImGui::Text("%s", T("feature.skin.intensity_of_secondary_specular_highlights", "Intensity of secondary specular highlights")); } - ImGui::SliderFloat("Fresnel F0", &settings.F0, 0.0f, 0.1f, "%.4f"); + ImGui::SliderFloat(T("feature.skin.fresnel_f0", "Fresnel F0"), &settings.F0, 0.0f, 0.1f, "%.4f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Fresnel reflectance"); + ImGui::Text("%s", T("feature.skin.fresnel_reflectance", "Fresnel reflectance")); } - ImGui::SliderFloat("Base Color Multiplier", &settings.BaseColorMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.base_color_multiplier", "Base Color Multiplier"), &settings.BaseColorMultiplier, 0.0f, 2.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Multiplier for the base color texture"); + ImGui::Text("%s", T("feature.skin.multiplier_for_the_base_color_texture", "Multiplier for the base color texture")); } ImGui::Spacing(); - ImGui::Text("Options for additional roughness and specular maps."); + ImGui::Text("%s", T("feature.skin.options_for_additional_roughness_and_specular_maps", "Options for additional roughness and specular maps.")); - ImGui::SliderFloat("Physical Main Roughness Multiplier", &settings.PhysicalMainRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Physical Second Roughness Multiplier", &settings.PhysicalSecondRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Physical Specular Multiplier", &settings.PhysicalSpecularStrength, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.physical_main_roughness_multiplier", "Physical Main Roughness Multiplier"), &settings.PhysicalMainRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.physical_second_roughness_multiplier", "Physical Second Roughness Multiplier"), &settings.PhysicalSecondRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.physical_specular_multiplier", "Physical Specular Multiplier"), &settings.PhysicalSpecularStrength, 0.0f, 2.0f, "%.2f"); ImGui::Spacing(); - ImGui::SliderFloat("Extra Edge Roughness", &settings.ExtraEdgeRoughness, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.extra_edge_roughness", "Extra Edge Roughness"), &settings.ExtraEdgeRoughness, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Extra roughness at the edges of the skin, to approximate peach fuzz on the face."); + ImGui::Text("%s", T("feature.skin.extra_roughness_at_the_edges_of_the_skin", "Extra roughness at the edges of the skin, to approximate peach fuzz on the face.")); } - ImGui::SliderFloat("Fuzz Strength", &settings.FuzzStrength, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.fuzz_strength", "Fuzz Strength"), &settings.FuzzStrength, 0.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Fuzz Roughness", &settings.FuzzRoughness, 0.1f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.fuzz_roughness", "Fuzz Roughness"), &settings.FuzzRoughness, 0.1f, 1.0f, "%.2f"); - ImGui::SliderFloat("Fuzz F0", &settings.FuzzF0, 0.0f, 0.5f, "%.4f"); + ImGui::SliderFloat(T("feature.skin.fuzz_f0", "Fuzz F0"), &settings.FuzzF0, 0.0f, 0.5f, "%.4f"); ImGui::Spacing(); - ImGui::Checkbox("Enable SSS Transmission", &settings.UseSSS); + ImGui::Checkbox(T("feature.skin.enable_sss_transmission", "Enable SSS Transmission"), &settings.UseSSS); - ImGui::SliderFloat("Translucency", &settings.Translucency, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.translucency", "Translucency"), &settings.Translucency, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Translucency of the SSS Transmittance effect"); + ImGui::Text("%s", T("feature.skin.translucency_of_the_sss_transmittance_effect", "Translucency of the SSS Transmittance effect")); } - ImGui::SliderFloat("SSS Width", &settings.sssWidth, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.sss_width", "SSS Width"), &settings.sssWidth, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Width of the SSS Transmittance effect"); + ImGui::Text("%s", T("feature.skin.width_of_the_sss_transmittance_effect", "Width of the SSS Transmittance effect")); } ImGui::Spacing(); - ImGui::SliderFloat("Extra Skin Wetness", &settings.ExtraSkinWetness, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.extra_skin_wetness", "Extra Skin Wetness"), &settings.ExtraSkinWetness, 0.0f, 2.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort."); + ImGui::Text("%s", T("feature.skin.adds_a_constant_layer_of_wetness_to_all", "Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort.")); } - ImGui::SliderFloat("Wetness Fade Out Time", &settings.WetFadeTime, 0.0f, 50.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.wetness_fade_out_time", "Wetness Fade Out Time"), &settings.WetFadeTime, 0.0f, 50.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How many seconds it takes for skin to fully dry after leaving water. Higher values mean wetness lingers longer."); + ImGui::Text("%s", T("feature.skin.how_many_seconds_it_takes_for_skin_to", "How many seconds it takes for skin to fully dry after leaving water. Higher values mean wetness lingers longer.")); } if (isDynamicWetnessAvailable) { - ImGui::Text("Dynamic Wetness detected."); - ImGui::Checkbox("Use Dynamic Wetness", &settings.UseDynamicWetness); + ImGui::Text("%s", T("feature.skin.dynamic_wetness_detected", "Dynamic Wetness detected.")); + ImGui::Checkbox(T("feature.skin.use_dynamic_wetness", "Use Dynamic Wetness"), &settings.UseDynamicWetness); } else { settings.UseDynamicWetness = false; } if (!settings.UseDynamicWetness) { - ImGui::SliderFloat("Stamina Threshold for Sweat", &settings.StartSweat, 0.0f, 1.0f, "%.2f", + ImGui::SliderFloat(T("feature.skin.stamina_threshold_for_sweat", "Stamina Threshold for Sweat"), &settings.StartSweat, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The character starts sweating when their stamina drops below this percentage. For example, 0.75 means sweat appears below 75%% stamina."); + ImGui::Text(T("feature.skin.the_character_starts_sweating_when_their_stamina_drops", "The character starts sweating when their stamina drops below this percentage. For example, 0.75 means sweat appears below 75%% stamina.")); } - ImGui::SliderFloat("Full Sweat Threshold", &settings.FullSweat, 0.0f, 1.0f, "%.2f", + ImGui::SliderFloat(T("feature.skin.full_sweat_threshold", "Full Sweat Threshold"), &settings.FullSweat, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The character reaches maximum sweat when stamina drops below this percentage. For example, 0.15 means full sweat below 15%% stamina."); + ImGui::Text(T("feature.skin.the_character_reaches_maximum_sweat_when_stamina_drops", "The character reaches maximum sweat when stamina drops below this percentage. For example, 0.15 means full sweat below 15%% stamina.")); } } - ImGui::SliderFloat("Wetness Perlin Noise Scale", &settings.WetParams.x, 0.0f, 1024.0f, "%1.f"); + ImGui::SliderFloat(T("feature.skin.wetness_perlin_noise_scale", "Wetness Perlin Noise Scale"), &settings.WetParams.x, 0.0f, 1024.0f, "%1.f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls the size of the wet/dry pattern on skin. Higher values create a finer, more detailed pattern; lower values produce larger, broader wet patches."); + ImGui::Text("%s", T("feature.skin.controls_the_size_of_the_wet_dry_pattern", "Controls the size of the wet/dry pattern on skin. Higher values create a finer, more detailed pattern; lower values produce larger, broader wet patches.")); } - ImGui::SliderFloat("Wetness Perlin Noise Lacunarity", &settings.WetParams.y, 0.0f, 2.0f, "%.1f"); + ImGui::SliderFloat(T("feature.skin.wetness_perlin_noise_lacunarity", "Wetness Perlin Noise Lacunarity"), &settings.WetParams.y, 0.0f, 2.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls how much fine detail is added to the wetness pattern. Higher values add more small-scale variation on top of the base pattern."); + ImGui::Text("%s", T("feature.skin.controls_how_much_fine_detail_is_added_to", "Controls how much fine detail is added to the wetness pattern. Higher values add more small-scale variation on top of the base pattern.")); } - ImGui::SliderFloat("Wetness Perlin Noise Persistence", &settings.WetParams.z, 0.0f, 20.0f, "%.2f"); + ImGui::SliderFloat(T("feature.skin.wetness_perlin_noise_persistence", "Wetness Perlin Noise Persistence"), &settings.WetParams.z, 0.0f, 20.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls the overall contrast and roughness of the wetness pattern. Higher values make the pattern more pronounced and varied."); + ImGui::Text("%s", T("feature.skin.controls_the_overall_contrast_and_roughness_of_the", "Controls the overall contrast and roughness of the wetness pattern. Higher values make the pattern more pronounced and varied.")); } - ImGui::SliderFloat("Wetness Normal Scale", &settings.WetParams.w, 0.0f, 20.0f, "%.1f"); + ImGui::SliderFloat(T("feature.skin.wetness_normal_scale", "Wetness Normal Scale"), &settings.WetParams.w, 0.0f, 20.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls how bumpy wet skin appears. Higher values create more visible surface ripples and distortion on wet areas."); + ImGui::Text("%s", T("feature.skin.controls_how_bumpy_wet_skin_appears_higher_values", "Controls how bumpy wet skin appears. Higher values create more visible surface ripples and distortion on wet areas.")); } ImGui::Spacing(); - ImGui::Checkbox("Enable Skin Detail", &settings.EnableSkinDetail); + ImGui::Checkbox(T("feature.skin.enable_skin_detail", "Enable Skin Detail"), &settings.EnableSkinDetail); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable skin detail texture"); + ImGui::Text("%s", T("feature.skin.enable_skin_detail_texture", "Enable skin detail texture")); } - ImGui::SliderFloat("Skin Detail Strength", &settings.SkinDetailStrength, -2.0f, 2.0f); + ImGui::SliderFloat(T("feature.skin.skin_detail_strength", "Skin Detail Strength"), &settings.SkinDetailStrength, -2.0f, 2.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Strength of skin detail texture"); + ImGui::Text("%s", T("feature.skin.strength_of_skin_detail_texture", "Strength of skin detail texture")); } - ImGui::SliderFloat("Skin Detail Tiling", &settings.SkinDetailTiling, 1.0f, 50.0f, "%1.f"); + ImGui::SliderFloat(T("feature.skin.skin_detail_tiling", "Skin Detail Tiling"), &settings.SkinDetailTiling, 1.0f, 50.0f, "%1.f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The more tiling, the more detailed the skin will be"); + ImGui::Text("%s", T("feature.skin.the_more_tiling_the_more_detailed_the_skin", "The more tiling, the more detailed the skin will be")); } - ImGui::SliderFloat("Body Tiling Multiplier", &settings.BodyTilingMultiplier, 0.5f, 5.0f, "%.1f"); + ImGui::SliderFloat(T("feature.skin.body_tiling_multiplier", "Body Tiling Multiplier"), &settings.BodyTilingMultiplier, 0.5f, 5.0f, "%.1f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Multiply the tiling for the body to match the face"); + ImGui::Text("%s", T("feature.skin.multiply_the_tiling_for_the_body_to_match", "Multiply the tiling for the body to match the face")); } - if (ImGui::Button("Reload Skin Detail Texture")) { + if (ImGui::Button(T("feature.skin.reload_skin_detail_texture", "Reload Skin Detail Texture"))) { ReloadSkinDetail(); } diff --git a/src/Features/Skin.h b/src/Features/Skin.h index ce023fa2a2..9e21044810 100644 --- a/src/Features/Skin.h +++ b/src/Features/Skin.h @@ -1,5 +1,7 @@ #pragma once +#include "I18n/I18n.h" + struct Skin : Feature { static Skin* GetSingleton() @@ -9,17 +11,18 @@ struct Skin : Feature } virtual inline std::string GetName() override { return "Advanced Skin"; } + virtual inline std::string GetDisplayName() override { return T("feature.skin.name", "Advanced Skin"); } virtual inline std::string GetShortName() override { return "Skin"; } virtual inline std::string_view GetShaderDefineName() override { return "CS_SKIN"; } - virtual std::string_view GetCategory() const override { return "Characters"; } + virtual std::string_view GetCategory() const override { return FeatureCategories::kCharacters; } virtual std::pair> GetFeatureSummary() override { return { - "Advanced Skin enhances character skin rendering with multiple techniques.", - { "Physically-based dual specular lobes for realistic skin highlights", - "Tiled skin detail textures for enhanced realism", - "Extra textures support for roughness, translucency, and more", - "Reworked wetness system for dynamic skin effects" } + T("feature.skin.description", "Advanced Skin enhances character skin rendering with multiple techniques."), + { T("feature.skin.key_feature_1", "Physically-based dual specular lobes for realistic skin highlights"), + T("feature.skin.key_feature_2", "Tiled skin detail textures for enhanced realism"), + T("feature.skin.key_feature_3", "Extra texture support for roughness, translucency, and wetness"), + T("feature.skin.key_feature_4", "Reworked wetness system for dynamic skin effects") } }; } virtual inline bool HasShaderDefine(RE::BSShader::Type t) override diff --git a/src/Features/SkySync.cpp b/src/Features/SkySync.cpp index 76d7b149d8..e2603401a5 100644 --- a/src/Features/SkySync.cpp +++ b/src/Features/SkySync.cpp @@ -1,4 +1,7 @@ #include "SkySync.h" +#include "../I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.sky_sync." NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( SkySync::Settings, @@ -15,60 +18,72 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void SkySync::DrawSettings() { - ImGui::Checkbox("Enabled", &settings.Enabled); + const char* sunPathNames[] = { + T(TKEY("sun_path_southern"), "Southern Sky"), + T(TKEY("sun_path_northern"), "Northern Sky"), + T(TKEY("sun_path_vanilla"), "Vanilla"), + T(TKEY("sun_path_custom"), "Custom") + }; + const char* moonLightSourceNames[] = { + T(TKEY("moon_light_source_brightest"), "Brightest"), + T(TKEY("moon_light_source_masser"), "Masser"), + T(TKEY("moon_light_source_secunda"), "Secunda") + }; + + ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), &settings.Enabled); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Enable or disable Sky Sync features."); + ImGui::TextUnformatted(T(TKEY("enabled_tooltip"), "Enable or disable Sky Sync features.")); } - ImGui::Checkbox("Use alternate sun path", &settings.UseAlternateSunPath); + ImGui::Checkbox(T(TKEY("use_alternate_sun_path"), "Use alternate sun path"), &settings.UseAlternateSunPath); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Calculate sun position based on time of day and season instead of vanilla movement."); + ImGui::TextUnformatted(T(TKEY("use_alternate_sun_path_tooltip"), "Calculate sun position based on time of day and season instead of vanilla movement.")); } if (settings.UseAlternateSunPath) { - if (ImGui::SliderInt("Sun path", &settings.SunPath, 0, static_cast(SunPath::Count) - 1, SunPathNames[settings.SunPath], ImGuiSliderFlags_AlwaysClamp)) + if (ImGui::SliderInt(T(TKEY("sun_path"), "Sun path"), &settings.SunPath, 0, static_cast(SunPath::Count) - 1, sunPathNames[settings.SunPath], ImGuiSliderFlags_AlwaysClamp)) SetSunAngle(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Choose the trajectory the sun takes across the sky."); + ImGui::TextUnformatted(T(TKEY("sun_path_tooltip"), "Choose the trajectory the sun takes across the sky.")); } if (settings.SunPath == static_cast(SunPath::Custom)) { - if (ImGui::SliderFloat("Custom angle", &settings.CustomAngle, -90.0f, 90.0f, "%.0f", ImGuiSliderFlags_AlwaysClamp)) + if (ImGui::SliderFloat(T(TKEY("custom_angle"), "Custom angle"), &settings.CustomAngle, -90.0f, 90.0f, "%.0f", ImGuiSliderFlags_AlwaysClamp)) SetSunAngle(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Set a custom angle for the sun's trajectory."); + ImGui::TextUnformatted(T(TKEY("custom_angle_tooltip"), "Set a custom angle for the sun's trajectory.")); } } } - ImGui::SliderInt("Moon light source", &settings.MoonLightSource, 0, static_cast(MoonLightSource::Count) - 1, MoonLightSourceNames[settings.MoonLightSource], ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderInt(T(TKEY("moon_light_source"), "Moon light source"), &settings.MoonLightSource, 0, static_cast(MoonLightSource::Count) - 1, moonLightSourceNames[settings.MoonLightSource], ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Select which moon casts shadows during the night."); + ImGui::TextUnformatted(T(TKEY("moon_light_source_tooltip"), "Select which moon casts shadows during the night.")); } - ImGui::SliderFloat("Min Shadow Elevation", &settings.MinShadowElevation, 0.0f, 45.0f, "%.1f deg", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("min_shadow_elevation"), "Min Shadow Elevation"), &settings.MinShadowElevation, 0.0f, 45.0f, "%.1f deg", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The minimum angle sunlight will set to. Caps shadow length. Higher = shorter shadows at sunset/sunrise."); + ImGui::Text("%s", T(TKEY("min_shadow_elevation_tooltip"), "The minimum angle sunlight will set to. Caps shadow length. Higher = shorter shadows at sunset/sunrise.")); } ImGui::Spacing(); ImGui::Spacing(); - if (ImGui::TreeNodeEx("Sun Position Offsets", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Moves sun height during sunrise/sunset. Reset weather to see changes."); - ImGui::SliderFloat("Sunrise Begin (Hours)", &settings.SunriseBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::TreeNodeEx(T(TKEY("sun_position_offsets"), "Sun Position Offsets"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::TextWrapped("%s", T(TKEY("sun_position_offsets_desc"), "Moves sun height during sunrise/sunset. Reset weather to see changes.")); + ImGui::SliderFloat(T(TKEY("sunrise_begin"), "Sunrise Begin (Hours)"), &settings.SunriseBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun starts rising."); + ImGui::TextUnformatted(T(TKEY("sunrise_begin_tooltip"), "Offset for when the sun starts rising.")); } - ImGui::SliderFloat("Sunrise End (Hours)", &settings.SunriseEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("sunrise_end"), "Sunrise End (Hours)"), &settings.SunriseEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun finishes rising."); + ImGui::TextUnformatted(T(TKEY("sunrise_end_tooltip"), "Offset for when the sun finishes rising.")); } - ImGui::SliderFloat("Sunset Begin (Hours)", &settings.SunsetBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("sunset_begin"), "Sunset Begin (Hours)"), &settings.SunsetBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun starts setting."); + ImGui::TextUnformatted(T(TKEY("sunset_begin_tooltip"), "Offset for when the sun starts setting.")); } - ImGui::SliderFloat("Sunset End (Hours)", &settings.SunsetEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("sunset_end"), "Sunset End (Hours)"), &settings.SunsetEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun finishes setting."); + ImGui::TextUnformatted(T(TKEY("sunset_end_tooltip"), "Offset for when the sun finishes setting.")); } ImGui::TreePop(); } @@ -560,3 +575,5 @@ inline float SkySync::SmoothStep(const float start, const float end, const float const float t = std::clamp((x - start) / (end - start), 0.0f, 1.0f); return t * t * (3.0f - 2.0f * t); } + +#undef I18N_KEY_PREFIX diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index efe6831898..651b987f1f 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -8,22 +8,21 @@ struct SkySync : Feature public: virtual inline std::string GetName() override { return "Sky Sync"; } + virtual std::string GetDisplayName() override { return T("feature.sky_sync.name", "Sky Sync"); } virtual inline std::string GetShortName() override { return "SkySync"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual std::string_view GetCategory() const override { return FeatureCategories::kSky; } virtual std::pair> GetFeatureSummary() override { - return { - "Synchronizes volumetric lighting and shadows with the actual sun and moon positions in the sky.", - { "Fixes the mismatch between the positions of the sun and moons and the lighting direction", - "Includes a configurable alternative sun path for more realistic and dramatic lighting", - "Smoothly switches the light source between the sun and moons based on visibility", - "Moon light source can be switched between Masser, Secunda, or the brightest", - "Automatic calculation of moon lighting intensity based on moon phase", - "Fixes the sun appearing higher on the horizon when the player gains altitude" } - }; - } + return { T("feature.sky_sync.description", "Synchronizes volumetric lighting and shadows with the actual sun and moon positions in the sky."), + { T("feature.sky_sync.key_feature_1", "Fixes the mismatch between the positions of the sun and moons and the lighting direction"), + T("feature.sky_sync.key_feature_2", "Includes a configurable alternative sun path for more realistic and dramatic lighting"), + T("feature.sky_sync.key_feature_3", "Smoothly switches the light source between the sun and moons based on visibility"), + T("feature.sky_sync.key_feature_4", "Moon light source can be switched between Masser, Secunda, or the brightest"), + T("feature.sky_sync.key_feature_5", "Automatic calculation of moon lighting intensity based on moon phase"), + T("feature.sky_sync.key_feature_6", "Fixes the sun appearing higher on the horizon when the player gains altitude") } }; + }; struct Settings { diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index 3b05dd7bec..b52305700c 100644 --- a/src/Features/Skylighting.cpp +++ b/src/Features/Skylighting.cpp @@ -1,9 +1,12 @@ #include "Skylighting.h" +#include "I18n/I18n.h" #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" +#define I18N_KEY_PREFIX "feature.skylighting." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Skylighting::Settings, MaxZenith, @@ -35,22 +38,21 @@ void Skylighting::ResetSkylighting() void Skylighting::DrawSettings() { - ImGui::Text("Minimum visibility values. Diffuse darkens objects. Specular removes the sky from reflections."); - ImGui::SliderFloat("Diffuse Min Visibility", &settings.MinDiffuseVisibility, 0.01f, 1.f, "%.2f"); - ImGui::SliderFloat("Specular Min Visibility", &settings.MinSpecularVisibility, 0.01f, 1.f, "%.2f"); + ImGui::Text("%s", T(TKEY("min_visibility_desc"), "Minimum visibility values. Diffuse darkens objects. Specular removes the sky from reflections.")); + ImGui::SliderFloat(T(TKEY("diffuse_min_visibility"), "Diffuse Min Visibility"), &settings.MinDiffuseVisibility, 0.01f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("specular_min_visibility"), "Specular Min Visibility"), &settings.MinSpecularVisibility, 0.01f, 1.f, "%.2f"); ImGui::Separator(); - if (ImGui::Button("Rebuild Skylighting")) + if (ImGui::Button(T(TKEY("rebuild"), "Rebuild Skylighting"))) ResetSkylighting(); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Changes below require rebuilding, a loading screen, or moving away from the current location to apply."); + ImGui::Text("%s", T(TKEY("rebuild_tooltip"), "Changes below require rebuilding, a loading screen, or moving away from the current location to apply.")); - ImGui::SliderAngle("Max Zenith Angle", &settings.MaxZenith, 0, 90); + ImGui::SliderAngle(T(TKEY("max_zenith"), "Max Zenith Angle"), &settings.MaxZenith, 0, 90); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Smaller angles creates more focused top-down shadow."); - + ImGui::Text("%s", T(TKEY("max_zenith_tooltip"), "Smaller angles creates more focused top-down shadow.")); } void Skylighting::SetupResources() @@ -634,4 +636,5 @@ RE::BSEventNotifyControl Skylighting::MenuOpenCloseEventHandler::ProcessEvent(co } return RE::BSEventNotifyControl::kContinue; -} \ No newline at end of file +} +#undef I18N_KEY_PREFIX diff --git a/src/Features/Skylighting.h b/src/Features/Skylighting.h index 71463341da..900f709ab2 100644 --- a/src/Features/Skylighting.h +++ b/src/Features/Skylighting.h @@ -9,21 +9,21 @@ struct Skylighting : Feature virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Skylighting"; } + virtual std::string GetDisplayName() override { return T("feature.skylighting.name", "Skylighting"); } virtual inline std::string GetShortName() override { return "Skylighting"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetShaderDefineName() override { return "SKYLIGHTING"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Simulates realistic ambient lighting by calculating sky occlusion and directional lighting, providing more accurate and natural illumination in outdoor environments.", - { "Sky occlusion calculation for ambient lighting", - "Directional skylighting based on environment geometry", - "Enhanced ambient lighting for outdoor scenes", - "Support for varying sky illumination intensities", - "Integration with existing lighting systems" } - }; - } + return { T("feature.skylighting.description", "Simulates realistic ambient lighting by calculating sky occlusion and directional lighting, providing more accurate and natural illumination in outdoor environments."), + { T("feature.skylighting.key_feature_1", "Sky occlusion calculation for ambient lighting"), + T("feature.skylighting.key_feature_2", "Directional skylighting based on environment geometry"), + T("feature.skylighting.key_feature_3", "Enhanced ambient lighting for outdoor scenes"), + T("feature.skylighting.key_feature_4", "Support for varying sky illumination intensities"), + T("feature.skylighting.key_feature_5", "Integration with existing lighting systems") } }; + }; + virtual bool HasShaderDefine(RE::BSShader::Type) override { return true; }; virtual void RestoreDefaultSettings() override; diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index b25d53ec24..384a7c7c61 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -1,9 +1,12 @@ #include "SubsurfaceScattering.h" +#include "../I18n/I18n.h" #include "Deferred.h" #include "ShaderCache.h" #include "State.h" +#define I18N_KEY_PREFIX "feature.sss." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(SubsurfaceScattering::DiffusionProfile, BlurRadius, Thickness, Strength, Falloff) @@ -20,75 +23,75 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void SubsurfaceScattering::DrawSettings() { - if (ImGui::TreeNodeEx("Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Character Lighting", (bool*)&settings.EnableCharacterLighting); + if (ImGui::TreeNodeEx(T(TKEY("settings"), "Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_character_lighting"), "Enable Character Lighting"), (bool*)&settings.EnableCharacterLighting); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Vanilla feature, not recommended."); + ImGui::Text("%s", T(TKEY("enable_character_lighting_tooltip"), "Vanilla feature, not recommended.")); } if (settings.EnableCharacterLighting) { - ImGui::SliderFloat("Strength", &settings.CharacterLightingStrength, 0, 5, "%.2f"); + ImGui::SliderFloat(T(TKEY("strength"), "Strength"), &settings.CharacterLightingStrength, 0, 5, "%.2f"); } - ImGui::RadioButton("Separable SSS", &settings.SSMode, 0); + ImGui::RadioButton(T(TKEY("separable_sss"), "Separable SSS"), &settings.SSMode, 0); ImGui::SameLine(); - ImGui::RadioButton("Burley", &settings.SSMode, 1); + ImGui::RadioButton(T(TKEY("burley"), "Burley"), &settings.SSMode, 1); if (settings.SSMode == 0) { - if (ImGui::TreeNodeEx("Base Profile", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Blur Radius", &settings.BaseProfile.BlurRadius, 0, 3, "%.2f"); + if (ImGui::TreeNodeEx(T(TKEY("base_profile"), "Base Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("blur_radius"), "Blur Radius"), &settings.BaseProfile.BlurRadius, 0, 3, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Blur radius."); + ImGui::Text("%s", T(TKEY("blur_radius_tooltip"), "Blur radius.")); } - ImGui::SliderFloat("Thickness", &settings.BaseProfile.Thickness, 0, 3, "%.2f"); + ImGui::SliderFloat(T(TKEY("thickness"), "Thickness"), &settings.BaseProfile.Thickness, 0, 3, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Blur radius relative to depth."); + ImGui::Text("%s", T(TKEY("thickness_tooltip"), "Blur radius relative to depth.")); } - updateKernels = updateKernels || ImGui::ColorEdit3("Strength", (float*)&settings.BaseProfile.Strength); - updateKernels = updateKernels || ImGui::ColorEdit3("Falloff", (float*)&settings.BaseProfile.Falloff); + updateKernels = updateKernels || ImGui::ColorEdit3(T(TKEY("strength"), "Strength"), (float*)&settings.BaseProfile.Strength); + updateKernels = updateKernels || ImGui::ColorEdit3(T(TKEY("falloff"), "Falloff"), (float*)&settings.BaseProfile.Falloff); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Human Profile", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Blur Radius", &settings.HumanProfile.BlurRadius, 0, 3, "%.2f"); + if (ImGui::TreeNodeEx(T(TKEY("human_profile"), "Human Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("blur_radius"), "Blur Radius"), &settings.HumanProfile.BlurRadius, 0, 3, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Blur radius."); + ImGui::Text("%s", T(TKEY("blur_radius_tooltip"), "Blur radius.")); } - ImGui::SliderFloat("Thickness", &settings.HumanProfile.Thickness, 0, 3, "%.2f"); + ImGui::SliderFloat(T(TKEY("thickness"), "Thickness"), &settings.HumanProfile.Thickness, 0, 3, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Blur radius relative to depth."); + ImGui::Text("%s", T(TKEY("thickness_tooltip"), "Blur radius relative to depth.")); } - updateKernels = updateKernels || ImGui::ColorEdit3("Strength", (float*)&settings.HumanProfile.Strength); - updateKernels = updateKernels || ImGui::ColorEdit3("Falloff", (float*)&settings.HumanProfile.Falloff); + updateKernels = updateKernels || ImGui::ColorEdit3(T(TKEY("strength"), "Strength"), (float*)&settings.HumanProfile.Strength); + updateKernels = updateKernels || ImGui::ColorEdit3(T(TKEY("falloff"), "Falloff"), (float*)&settings.HumanProfile.Falloff); ImGui::TreePop(); } } else if (settings.SSMode == 1) { - ImGui::SliderInt("Burley Samples", (int*)&settings.BurleySamples, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); - if (ImGui::TreeNodeEx("Base Profile", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::ColorEdit3("Mean Free Path Color", (float*)&settings.MeanFreePathBase); + ImGui::SliderInt(T(TKEY("burley_samples"), "Burley Samples"), (int*)&settings.BurleySamples, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::TreeNodeEx(T(TKEY("base_profile"), "Base Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::ColorEdit3(T(TKEY("mean_free_path_color"), "Mean Free Path Color"), (float*)&settings.MeanFreePathBase); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance."); + ImGui::Text("%s", T(TKEY("mean_free_path_color_tooltip"), "Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance.")); } - ImGui::SliderFloat("Mean Free Path Distance", &settings.MeanFreePathBase.w, 0.01f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("mean_free_path_distance"), "Mean Free Path Distance"), &settings.MeanFreePathBase.w, 0.01f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls the distance that Mean Free Path Color goes into subsurface."); + ImGui::Text("%s", T(TKEY("mean_free_path_distance_tooltip"), "Controls the distance that Mean Free Path Color goes into subsurface.")); } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Human Profile", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::ColorEdit3("Mean Free Path Color", (float*)&settings.MeanFreePathHuman); + if (ImGui::TreeNodeEx(T(TKEY("human_profile"), "Human Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::ColorEdit3(T(TKEY("mean_free_path_color"), "Mean Free Path Color"), (float*)&settings.MeanFreePathHuman); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance."); + ImGui::Text("%s", T(TKEY("mean_free_path_color_tooltip"), "Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance.")); } - ImGui::SliderFloat("Mean Free Path Distance", &settings.MeanFreePathHuman.w, 0.01f, 10.0f, "%.2f"); + ImGui::SliderFloat(T(TKEY("mean_free_path_distance"), "Mean Free Path Distance"), &settings.MeanFreePathHuman.w, 0.01f, 10.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Controls the distance that Mean Free Path Color goes into subsurface."); + ImGui::Text("%s", T(TKEY("mean_free_path_distance_tooltip"), "Controls the distance that Mean Free Path Color goes into subsurface.")); } ImGui::TreePop(); } @@ -455,3 +458,5 @@ void SubsurfaceScattering::Hooks::BSLightingShader_SetupGeometry::thunk(RE::BSSh globals::features::subsurfaceScattering.BSLightingShader_SetupSkin(Pass); func(This, Pass, RenderFlags); } + +#undef I18N_KEY_PREFIX diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index 9ea4fd58ca..3d8a32e7c0 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -66,22 +66,20 @@ struct SubsurfaceScattering : Feature RE::BGSKeyword* isBeastRaceKeyword = nullptr; virtual inline std::string GetName() override { return "Subsurface Scattering"; } + virtual std::string GetDisplayName() override { return T("feature.subsurface_scattering.name", "Subsurface Scattering"); } virtual inline std::string GetShortName() override { return "SubsurfaceScattering"; } virtual inline std::string_view GetShaderDefineName() override { return "SSS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kCharacters; } virtual std::pair> GetFeatureSummary() override { - return { - "Subsurface Scattering simulates light penetration through translucent materials like skin, creating more realistic character lighting.\n" - "This technique makes organic materials appear more lifelike and natural.", - { "Realistic skin lighting", - "Light penetration simulation", - "Separate profiles for different materials", - "Enhanced character appearance", - "Configurable scattering properties" } - }; - } + return { T("feature.subsurface_scattering.description", "Subsurface Scattering simulates light penetration through translucent materials like skin, creating more realistic character lighting.\nThis technique makes organic materials appear more lifelike and natural."), + { T("feature.subsurface_scattering.key_feature_1", "Realistic skin lighting"), + T("feature.subsurface_scattering.key_feature_2", "Light penetration simulation"), + T("feature.subsurface_scattering.key_feature_3", "Separate profiles for different materials"), + T("feature.subsurface_scattering.key_feature_4", "Enhanced character appearance"), + T("feature.subsurface_scattering.key_feature_5", "Configurable scattering properties") } }; + }; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index 26cdd6ee89..b298fdbb13 100644 --- a/src/Features/TerrainBlending.cpp +++ b/src/Features/TerrainBlending.cpp @@ -2,11 +2,14 @@ #include "Deferred.h" #include "Globals.h" +#include "I18n/I18n.h" #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" #include "VR.h" +#define I18N_KEY_PREFIX "feature.terrain_blending." + #include #include #include @@ -455,10 +458,10 @@ namespace void TerrainBlending::DrawSettings() { - ImGui::Checkbox("Enable Terrain Blending", (bool*)&settings.Enabled); + ImGui::Checkbox(T(TKEY("enable"), "Enable Terrain Blending"), (bool*)&settings.Enabled); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable seamless blending between terrain and objects."); + ImGui::Text("%s", T(TKEY("enable_tooltip"), "Enable seamless blending between terrain and objects.")); } } @@ -1058,3 +1061,4 @@ void TerrainBlending::RenderTerrainBlendingPasses() auto& mainDepth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; mainDepth.depthSRV = depthSRVBackup; } +#undef I18N_KEY_PREFIX diff --git a/src/Features/TerrainBlending.h b/src/Features/TerrainBlending.h index 20c5769a83..8f13612eb9 100644 --- a/src/Features/TerrainBlending.h +++ b/src/Features/TerrainBlending.h @@ -7,21 +7,21 @@ struct TerrainBlending : Feature public: virtual inline std::string GetName() override { return "Terrain Blending"; } + virtual std::string GetDisplayName() override { return T("feature.terrain_blending.name", "Terrain Blending"); } virtual inline std::string GetShortName() override { return "TerrainBlending"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetShaderDefineName() override { return "TERRAIN_BLENDING"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides seamless blending between terrain and objects, eliminating harsh transitions where objects meet the ground for more natural-looking landscapes.", - { "Seamless terrain-to-object blending transitions", - "Advanced depth buffer manipulation for smooth integration", - "Support for alternative terrain rendering modes", - "Multi-pass rendering optimization for complex scenes", - "Enhanced visual continuity in landscape interactions" } - }; - } + return { T("feature.terrain_blending.description", "Provides seamless blending between terrain and objects, eliminating harsh transitions where objects meet the ground for more natural-looking landscapes."), + { T("feature.terrain_blending.key_feature_1", "Seamless terrain-to-object blending transitions"), + T("feature.terrain_blending.key_feature_2", "Advanced depth buffer manipulation for smooth integration"), + T("feature.terrain_blending.key_feature_3", "Support for alternative terrain rendering modes"), + T("feature.terrain_blending.key_feature_4", "Multi-pass rendering optimization for complex scenes"), + T("feature.terrain_blending.key_feature_5", "Enhanced visual continuity in landscape interactions") } }; + }; + virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } virtual bool SupportsVR() override { return true; } diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index c83ee5d79e..c477a27ce0 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -7,21 +7,20 @@ struct TerrainHelper : Feature public: virtual inline std::string GetName() override { return "Terrain Helper"; } + virtual std::string GetDisplayName() override { return T("feature.terrain_helper.name", "Terrain Helper"); } virtual inline std::string GetShortName() override { return "TerrainHelper"; } virtual inline std::string_view GetShaderDefineName() override { return "TERRAIN_HELPER"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides enhanced terrain material support for terrain mods that require additional texture slots and parallax mapping capabilities.", - { "Extended texture slot support for terrain materials", - "Parallax mapping integration for terrain textures", - "Automatic terrain material detection and setup", - "Support for advanced terrain modifications", - "Compatibility layer for terrain enhancement mods" } - }; - } + return { T("feature.terrain_helper.description", "Provides enhanced terrain material support for terrain mods that require additional texture slots and parallax mapping capabilities."), + { T("feature.terrain_helper.key_feature_1", "Extended texture slot support for terrain materials"), + T("feature.terrain_helper.key_feature_2", "Parallax mapping integration for terrain textures"), + T("feature.terrain_helper.key_feature_3", "Automatic terrain material detection and setup"), + T("feature.terrain_helper.key_feature_4", "Support for advanced terrain modifications"), + T("feature.terrain_helper.key_feature_5", "Compatibility layer for terrain enhancement mods") } }; + }; struct Settings { diff --git a/src/Features/TerrainShadows.cpp b/src/Features/TerrainShadows.cpp index b881a7836b..6a07ac802b 100644 --- a/src/Features/TerrainShadows.cpp +++ b/src/Features/TerrainShadows.cpp @@ -3,9 +3,12 @@ #include #include +#include "I18n/I18n.h" #include "State.h" #include "Util.h" +#define I18N_KEY_PREFIX "feature.terrain_shadows." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( TerrainShadows::Settings, EnableTerrainShadow) @@ -22,9 +25,9 @@ void TerrainShadows::SaveSettings(json& o_json) void TerrainShadows::DrawSettings() { - ImGui::Checkbox("Enable Terrain Shadow", &settings.EnableTerrainShadow); + ImGui::Checkbox(T(TKEY("enable_terrain_shadow"), "Enable Terrain Shadow"), &settings.EnableTerrainShadow); - if (ImGui::CollapsingHeader("Debug")) { + if (ImGui::CollapsingHeader(T(TKEY("debug"), "Debug"))) { std::string curr_worldspace = "N/A"; std::string curr_worldspace_name = "N/A"; auto tes = RE::TES::GetSingleton(); @@ -50,7 +53,7 @@ void TerrainShadows::DrawSettings() } ImGui::Unindent(); - if (ImGui::TreeNode("Buffer Viewer")) { + if (ImGui::TreeNode(T(TKEY("buffer_viewer"), "Buffer Viewer"))) { static float debugRescale = .1f; ImGui::SliderFloat("View Resize", &debugRescale, 0.f, 1.f); @@ -305,6 +308,7 @@ void TerrainShadows::Precompute() texShadowHeight->CreateSRV(srvDesc); texShadowHeight->CreateUAV(uavDesc); } +#undef I18N_KEY_PREFIX needPrecompute = false; } diff --git a/src/Features/TerrainShadows.h b/src/Features/TerrainShadows.h index ab39090a6f..3d824de82c 100644 --- a/src/Features/TerrainShadows.h +++ b/src/Features/TerrainShadows.h @@ -7,20 +7,20 @@ struct TerrainShadows : public Feature { public: virtual inline std::string GetName() override { return "Terrain Shadows"; } + virtual std::string GetDisplayName() override { return T("feature.terrain_shadows.name", "Terrain Shadows"); } virtual inline std::string GetShortName() override { return "TerrainShadows"; } virtual inline std::string_view GetShaderDefineName() override { return "TERRAIN_SHADOWS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override { - return { - "Adds realistic shadow casting from terrain features using heightmap data to create accurate terrain shadows that enhance depth perception and visual realism.", - { "Heightmap-based terrain shadow calculation", - "Dynamic shadow updates based on sun position", - "Support for custom heightmap files", - "Real-time shadow preprocessing and computation", - "Integration with existing shadow systems" } - }; - } + return { T("feature.terrain_shadows.description", "Adds realistic shadow casting from terrain features using heightmap data to create accurate terrain shadows that enhance depth perception and visual realism."), + { T("feature.terrain_shadows.key_feature_1", "Heightmap-based terrain shadow calculation"), + T("feature.terrain_shadows.key_feature_2", "Dynamic shadow updates based on sun position"), + T("feature.terrain_shadows.key_feature_3", "Support for custom heightmap files"), + T("feature.terrain_shadows.key_feature_4", "Real-time shadow preprocessing and computation"), + T("feature.terrain_shadows.key_feature_5", "Integration with existing shadow systems") } }; + }; + virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } struct Settings diff --git a/src/Features/TerrainVariation.cpp b/src/Features/TerrainVariation.cpp index fa76cae5ee..e383d0152c 100644 --- a/src/Features/TerrainVariation.cpp +++ b/src/Features/TerrainVariation.cpp @@ -1,9 +1,12 @@ #include "TerrainVariation.h" #include "../FeatureBuffer.h" #include "../Globals.h" +#include "../I18n/I18n.h" #include "../State.h" #include "../Util.h" +#define I18N_KEY_PREFIX "feature.terrain_variation." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( TerrainVariation::Settings, enableTilingFix, @@ -12,33 +15,33 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void TerrainVariation::DrawSettings() { bool oldEnabled = settings.enableTilingFix; - ImGui::Checkbox("Enable Terrain Tiling Fix", (bool*)&settings.enableTilingFix); + ImGui::Checkbox(T(TKEY("enable_tiling_fix"), "Enable Terrain Tiling Fix"), (bool*)&settings.enableTilingFix); if (oldEnabled != (bool)settings.enableTilingFix) { // Update the shader settings when the checkbox is toggled UpdateShaderSettings(); logger::info("TerrainVariation setting changed to: {}", settings.enableTilingFix); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Reduces the repeating pattern effect on terrain textures.\n" - "This technique creates more natural-looking terrain by adding variation to texture sampling."); + ImGui::Text("%s", T(TKEY("enable_tiling_fix_tooltip"), + "Reduces the repeating pattern effect on terrain textures.\nThis technique creates more natural-looking terrain by adding variation to texture sampling.")); } ImGui::Separator(); bool oldLODEnabled = settings.enableLODTerrainTilingFix; - ImGui::Checkbox("Apply to LOD Terrain", (bool*)&settings.enableLODTerrainTilingFix); + ImGui::Checkbox(T(TKEY("apply_to_lod_terrain"), "Apply to LOD Terrain"), (bool*)&settings.enableLODTerrainTilingFix); if (oldLODEnabled != (bool)settings.enableLODTerrainTilingFix) { UpdateShaderSettings(); logger::info("TerrainVariation LOD setting changed to: {}", settings.enableLODTerrainTilingFix); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Applies the tiling fix to LOD terrain objects.\n" - "This helps reduce the visible tiling effect on distant terrain."); + ImGui::Text("%s", T(TKEY("apply_to_lod_terrain_tooltip"), + "Applies the tiling fix to LOD terrain objects.\nThis helps reduce the visible tiling effect on distant terrain.")); } } +#undef I18N_KEY_PREFIX + void TerrainVariation::UpdateShaderSettings() { if (!globals::state) { @@ -76,4 +79,4 @@ void TerrainVariation::RestoreDefaultSettings() bool TerrainVariation::DrawFailLoadMessage() const { return false; -} \ No newline at end of file +} diff --git a/src/Features/TerrainVariation.h b/src/Features/TerrainVariation.h index 1434b8d4de..83bf3385f1 100644 --- a/src/Features/TerrainVariation.h +++ b/src/Features/TerrainVariation.h @@ -7,6 +7,7 @@ struct TerrainVariation : Feature public: virtual inline std::string GetName() override { return "Terrain Variation"; } + virtual std::string GetDisplayName() override { return T("feature.terrain_variation.name", "Terrain Variation"); } virtual inline std::string GetShortName() override { return "TerrainVariation"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetShaderDefineName() override { return "TERRAIN_VARIATION"; } @@ -20,15 +21,12 @@ struct TerrainVariation : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Terrain Variation reduces the repeating pattern effect on terrain textures.\n" - "This technique creates more natural-looking terrain by adding variation to texture sampling.", - { "Reduces terrain texture tiling", - "Adjustable distance-based blending", - "Improved terrain visual quality", - "Compatible with Extended Materials parallax" } - }; - } + return { T("feature.terrain_variation.description", "Terrain Variation reduces the repeating pattern effect on terrain textures.\nThis technique creates more natural-looking terrain by adding variation to texture sampling."), + { T("feature.terrain_variation.key_feature_1", "Reduces terrain texture tiling"), + T("feature.terrain_variation.key_feature_2", "Adjustable distance-based blending"), + T("feature.terrain_variation.key_feature_3", "Improved terrain visual quality"), + T("feature.terrain_variation.key_feature_4", "Compatible with Extended Materials parallax") } }; + }; struct Settings { diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 05e2c5e224..d3121345be 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -1,9 +1,12 @@ #include "UnifiedWater.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Menu/ThemeManager.h" #include "Util.h" +#define I18N_KEY_PREFIX "feature.unified_water." + #include "RE/L/LoadingMenu.h" #include "RE/M/MapMenu.h" #include "RE/P/PlayerCharacter.h" @@ -79,22 +82,22 @@ void UnifiedWater::RestoreDefaultSettings() void UnifiedWater::DrawSettings() { - ImGui::Checkbox("Use Optimised Meshes", &settings.UseOptimisedMeshes); + ImGui::Checkbox(T(TKEY("use_optimised_meshes"), "Use Optimised Meshes"), &settings.UseOptimisedMeshes); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Uses meshes with significantly lower tri-count for improved performance with no visual quality loss.\n" - "Will only affect newly created water - requires a change of location or game restart to take effect."); + ImGui::Text("%s", T(TKEY("use_optimised_meshes_tooltip"), + "Uses meshes with significantly lower tri-count for improved performance with no visual quality loss.\n" + "Will only affect newly created water - requires a change of location or game restart to take effect.")); } ImGui::Spacing(); - if (ImGui::TreeNodeEx("Debug", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Button("Regenerate Flowmap") && flowmap) { + if (ImGui::TreeNodeEx(T(TKEY("debug"), "Debug"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Button(T(TKEY("regenerate_flowmap"), "Regenerate Flowmap")) && flowmap) { if (flowmap->RegenerateAndLoadFlowmap()) SetFlowmapTex(); } - if (ImGui::Button("Regenerate Caches") && waterCache) + if (ImGui::Button(T(TKEY("regenerate_caches"), "Regenerate Caches")) && waterCache) waterCache->RegenerateCaches(); ImGui::TreePop(); @@ -131,7 +134,7 @@ void UnifiedWater::DrawOverlay() auto& themeSettings = Menu::GetSingleton()->GetTheme(); if (waterCache->IsBuildRunning()) { - auto progressTitle = fmt::format("Generating Water Cache:"); + auto progressTitle = T(TKEY("generating_water_cache"), "Generating Water Cache:"); auto percent = static_cast(snapshot.completed) / static_cast(snapshot.total); auto progressOverlay = fmt::format("{}/{} ({:2.1f}%)", snapshot.completed, snapshot.total, 100 * percent); @@ -140,7 +143,7 @@ void UnifiedWater::DrawOverlay() ImGui::End(); return; } - ImGui::TextUnformatted(progressTitle.c_str()); + ImGui::TextUnformatted(progressTitle); ImGui::ProgressBar(percent, ImVec2(0.0f, 0.0f), progressOverlay.c_str()); ImGui::End(); @@ -151,7 +154,7 @@ void UnifiedWater::DrawOverlay() return; } - ImGui::TextColored(themeSettings.StatusPalette.Error, "ERROR: Water cache generation failed for %d WorldSpaces. Check installation and CommunityShaders.log", snapshot.failed); + ImGui::TextColored(themeSettings.StatusPalette.Error, T("feature.unified_water.error_water_cache_generation_failed_for_worldspaces_check", "ERROR: Water cache generation failed for %d WorldSpaces. Check installation and CommunityShaders.log"), snapshot.failed); ImGui::End(); } @@ -706,3 +709,4 @@ void UnifiedWater::TESWaterSystem_UpdateDisplacementMeshPosition::thunk(RE::TESW // Previously the values were calculated relative to the 5x5 flow grid *singleton.gDisplacementCellTexCoordOffset = float4(posX + offsetX, height - (posY + offsetY), posX, 1 - posY); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 578347895e..4a1e616c33 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -8,19 +8,19 @@ struct UnifiedWater : OverlayFeature { virtual inline std::string GetName() override { return "Unified Water"; } + virtual std::string GetDisplayName() override { return T("feature.unified_water.name", "Unified Water"); } virtual inline std::string GetShortName() override { return "UnifiedWater"; } virtual inline std::string_view GetShaderDefineName() override { return "UNIFIED_WATER"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kWater; } virtual std::pair> GetFeatureSummary() override { - return { - "Unified Water provides a comprehensive fix to water LOD mismatch by replacing distant water tiles with LOD0 (Close Water).", - { "Unifies distant and close water appearance, streamlining all lighting visuals.", - "Completely and fundamentally resolves water LOD mismatch issues.", - "Provides background systems for water geometry rendering, allowing more advanced water effects.", - "Improves vanilla performance by using optimized water meshes for distant water." } - }; - } + return { T("feature.unified_water.description", "Unified Water provides a comprehensive fix to water LOD mismatch by replacing distant water tiles with LOD0 (Close Water)."), + { T("feature.unified_water.key_feature_1", "Unifies distant and close water appearance, streamlining all lighting visuals."), + T("feature.unified_water.key_feature_2", "Completely and fundamentally resolves water LOD mismatch issues."), + T("feature.unified_water.key_feature_3", "Provides background systems for water geometry rendering, allowing more advanced water effects."), + T("feature.unified_water.key_feature_4", "Improves vanilla performance by using optimized water meshes for distant water.") } }; + }; + virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } struct Settings diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 99c87ed7cc..649b1af626 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -1,5 +1,6 @@ #include "Upscaling.h" +#include "../I18n/I18n.h" #include "Deferred.h" #include "HDRDisplay.h" #include "Hooks.h" @@ -15,6 +16,8 @@ #include #include +#define I18N_KEY_PREFIX "feature.upscaling." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Upscaling::Settings, upscaleMethod, @@ -180,7 +183,10 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( void Upscaling::DrawSettings() { // Display upscaling options in the UI - std::vector upscaleModes = { "None", "TAA" }; + std::vector upscaleModes = { + T(TKEY("method_none"), "None"), + T(TKEY("method_taa"), "TAA") + }; std::string fsrLabel = "AMD FSR 3.1"; upscaleModes.push_back(fsrLabel); @@ -205,7 +211,7 @@ void Upscaling::DrawSettings() std::vector modeLabels; for (uint32_t i = 0; i <= availableModes; ++i) modeLabels.push_back(upscaleModes[i].c_str()); - ImGui::Combo("Method", (int*)currentUpscaleMode, modeLabels.data(), (int)modeLabels.size()); + ImGui::Combo(T(TKEY("method"), "Method"), (int*)currentUpscaleMode, modeLabels.data(), (int)modeLabels.size()); *currentUpscaleMode = std::min(availableModes, *currentUpscaleMode); @@ -224,8 +230,20 @@ void Upscaling::DrawSettings() // Display upscaling settings if applicable if (upscaleMethod != UpscaleMethod::kNONE && upscaleMethod != UpscaleMethod::kTAA) { - const char* upscalePresetsDLSS[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "DLAA" }; - const char* upscalePresets[] = { "Ultra Performance", "Performance", "Balanced", "Quality", "Native AA" }; + const char* upscalePresetsDLSS[] = { + T(TKEY("preset_ultra_performance"), "Ultra Performance"), + T(TKEY("preset_performance"), "Performance"), + T(TKEY("preset_balanced"), "Balanced"), + T(TKEY("preset_quality"), "Quality"), + T(TKEY("preset_dlaa"), "DLAA") + }; + const char* upscalePresets[] = { + T(TKEY("preset_ultra_performance"), "Ultra Performance"), + T(TKEY("preset_performance"), "Performance"), + T(TKEY("preset_balanced"), "Balanced"), + T(TKEY("preset_quality"), "Quality"), + T(TKEY("preset_native_aa"), "Native AA") + }; // Compute a safe preset index (4 - qualityMode) clamped to [0,4] to avoid negative/overflow indexing int presetIndex = 0; @@ -247,21 +265,28 @@ void Upscaling::DrawSettings() // Format the label with preset name and resolution scale std::string labelWithScale = std::format("{} ( {:.2f}x )", baseLabel, (resolutionScale.x + resolutionScale.y) * 0.5f); - ImGui::SliderInt("Upscale Preset", (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); + ImGui::SliderInt(T(TKEY("upscale_preset"), "Upscale Preset"), (int*)&settings.qualityMode, 0, 4, labelWithScale.c_str()); } if (upscaleMethod == UpscaleMethod::kFSR) { - ImGui::SliderFloat("Sharpness", &settings.sharpnessFSR, 0.0f, 1.0f, "%.1f"); + ImGui::SliderFloat(T(TKEY("sharpness"), "Sharpness"), &settings.sharpnessFSR, 0.0f, 1.0f, "%.1f"); } else if (upscaleMethod == UpscaleMethod::kDLSS) { - ImGui::SliderFloat("Sharpness", &settings.sharpnessDLSS, 0.0f, 1.0f, "%.1f"); - - const char* presets[] = { "Default", "Preset J", "Preset K", "Preset L", "Preset M" }; - ImGui::Combo("DLSS Model Preset", (int*)&settings.presetDLSS, presets, 5); + ImGui::SliderFloat(T(TKEY("sharpness"), "Sharpness"), &settings.sharpnessDLSS, 0.0f, 1.0f, "%.1f"); + + const char* presets[] = { + T(TKEY("dlss_model_preset_default"), "Default"), + T(TKEY("dlss_model_preset_j"), "Preset J"), + T(TKEY("dlss_model_preset_k"), "Preset K"), + T(TKEY("dlss_model_preset_l"), "Preset L"), + T(TKEY("dlss_model_preset_m"), "Preset M") + }; + ImGui::Combo(T(TKEY("dlss_model_preset"), "DLSS Model Preset"), (int*)&settings.presetDLSS, presets, 5); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Choose which DLSS AI model preset to use."); - ImGui::Text("Each model offers different visual quality, performance, and motion stability."); - ImGui::Text("Set to 'Default' for automatic selection based on your Upscale Preset and hardware."); - ImGui::Text("Changing this setting requires a restart to take effect."); + ImGui::Text("%s", T(TKEY("dlss_model_preset_tooltip"), + "Choose which DLSS AI model preset to use.\n" + "Each model offers different visual quality, performance, and motion stability.\n" + "Set to 'Default' for automatic selection based on your Upscale Preset and hardware.\n" + "Changing this setting requires a restart to take effect.")); } } } @@ -269,13 +294,18 @@ void Upscaling::DrawSettings() const bool frameGenerationDx12PathActive = IsFrameGenerationDx12PathActive(); if (!globals::game::isVR) { - if (ImGui::TreeNodeEx("Frame Generation", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Frame Generation interpolates real frames with generated ones for a smoother experience"); - ImGui::Text("Uses AMD FSR Frame Generation technology"); + if (ImGui::TreeNodeEx(T(TKEY("frame_generation"), "Frame Generation"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", T(TKEY("frame_generation_desc"), + "Frame Generation interpolates real frames with generated ones for a smoother experience")); + ImGui::Text("%s", T(TKEY("frame_generation_tech"), + "Uses AMD FSR Frame Generation technology")); if (HasFrameGenModule()) - ImGui::Text("AMD FSR Frame Generation is available."); - ImGui::Text("Requires a D3D11 to D3D12 proxy which can create compatibility issues"); - ImGui::Text("Toggling this setting requires a restart to work correctly"); + ImGui::Text("%s", T(TKEY("frame_generation_available"), + "AMD FSR Frame Generation is available.")); + ImGui::Text("%s", T(TKEY("frame_generation_proxy_note"), + "Requires a D3D11 to D3D12 proxy which can create compatibility issues")); + ImGui::Text("%s", T(TKEY("frame_generation_restart_note"), + "Toggling this setting requires a restart to work correctly")); bool onlyRequiresRestart = true; @@ -304,14 +334,14 @@ void Upscaling::DrawSettings() Util::Text::Warning("Warning: Requires restart"); bool fgEnabled = settings.frameGenerationMode != 0; - if (ImGui::Checkbox("Frame Generation", &fgEnabled)) + if (ImGui::Checkbox(T(TKEY("frame_generation"), "Frame Generation"), &fgEnabled)) settings.frameGenerationMode = fgEnabled ? 1 : 0; if (!frameGenerationDx12PathActive) ImGui::BeginDisabled(); bool flEnabled = settings.frameLimitMode != 0; - if (ImGui::Checkbox("Frame Limit (Variable Refresh Rate)", &flEnabled)) + if (ImGui::Checkbox(T(TKEY("frame_limit_vrr"), "Frame Limit (Variable Refresh Rate)"), &flEnabled)) settings.frameLimitMode = flEnabled ? 1 : 0; if (!frameGenerationDx12PathActive) @@ -319,70 +349,70 @@ void Upscaling::DrawSettings() ImGui::TextWrapped("Allows frame generation to function on low refresh rate monitors. Detected: %.2f Hz", refreshRate); bool fgForce = settings.frameGenerationForceEnable != 0; - if (ImGui::Checkbox("Force Enable Frame Generation", &fgForce)) + if (ImGui::Checkbox(T(TKEY("force_enable_frame_generation"), "Force Enable Frame Generation"), &fgForce)) settings.frameGenerationForceEnable = fgForce ? 1 : 0; - ImGui::Checkbox("Frame Generation in Menus", &settings.frameGenerationAllowInMenus); + ImGui::Checkbox(T(TKEY("frame_generation_in_menus"), "Frame Generation in Menus"), &settings.frameGenerationAllowInMenus); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Keeps frame generation active while game menus are open."); - ImGui::TextUnformatted("May feel smoother, but increases menu input latency."); + ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_1"), "Keeps frame generation active while game menus are open.")); + ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_2"), "May feel smoother, but increases menu input latency.")); } ImGui::TreePop(); } } - if (streamline.reflexSupportedOnCurrentAdapter && ImGui::TreeNodeEx("NVIDIA Reflex", ImGuiTreeNodeFlags_DefaultOpen)) { + if (streamline.reflexSupportedOnCurrentAdapter && ImGui::TreeNodeEx(T(TKEY("nvidia_reflex"), "NVIDIA Reflex"), ImGuiTreeNodeFlags_DefaultOpen)) { const bool reflexBlockedByFrameGeneration = frameGenerationDx12PathActive; const bool reflexAvailable = streamline.initialized && streamline.featureReflex; const bool reflexControlsAvailable = reflexAvailable && !reflexBlockedByFrameGeneration; const bool markerOptimizationAvailable = reflexControlsAvailable && streamline.featurePCL; if (reflexBlockedByFrameGeneration) { - ImGui::TextDisabled("Reflex is unavailable while the DX12 frame-generation swapchain is active."); + ImGui::TextDisabled("%s", T(TKEY("reflex_blocked_by_fg"), "Reflex is unavailable while the DX12 frame-generation swapchain is active.")); } if (!reflexAvailable) { - ImGui::TextDisabled("Reflex is not available. Ensure sl.reflex.dll is present and restart."); + ImGui::TextDisabled("%s", T(TKEY("reflex_not_available"), "Reflex is not available. Ensure sl.reflex.dll is present and restart.")); } if (!reflexControlsAvailable) ImGui::BeginDisabled(); - ImGui::Checkbox("Low Latency Mode", &settings.reflexLowLatencyMode); + ImGui::Checkbox(T(TKEY("low_latency_mode"), "Low Latency Mode"), &settings.reflexLowLatencyMode); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Cuts input delay by syncing CPU work closer to the GPU."); - ImGui::TextUnformatted("Can reduce max FPS a little, but usually feels more responsive."); + ImGui::TextUnformatted(T(TKEY("low_latency_mode_tooltip_1"), "Cuts input delay by syncing CPU work closer to the GPU.")); + ImGui::TextUnformatted(T(TKEY("low_latency_mode_tooltip_2"), "Can reduce max FPS a little, but usually feels more responsive.")); } if (!settings.reflexLowLatencyMode) ImGui::BeginDisabled(); - ImGui::Checkbox("Low Latency Boost", &settings.reflexLowLatencyBoost); + ImGui::Checkbox(T(TKEY("low_latency_boost"), "Low Latency Boost"), &settings.reflexLowLatencyBoost); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Keeps GPU clocks higher to avoid latency spikes at low GPU load."); - ImGui::TextUnformatted("Useful if frametime jumps; costs extra power and heat."); + ImGui::TextUnformatted(T(TKEY("low_latency_boost_tooltip_1"), "Keeps GPU clocks higher to avoid latency spikes at low GPU load.")); + ImGui::TextUnformatted(T(TKEY("low_latency_boost_tooltip_2"), "Useful if frametime jumps; costs extra power and heat.")); } if (!markerOptimizationAvailable) ImGui::BeginDisabled(); - ImGui::Checkbox("Use Markers To Optimize", &settings.reflexUseMarkersToOptimize); + ImGui::Checkbox(T(TKEY("use_markers_to_optimize"), "Use Markers To Optimize"), &settings.reflexUseMarkersToOptimize); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Uses frame markers for tighter Reflex timing."); - ImGui::TextUnformatted("Try On first; turn Off if it causes stutter on your setup."); + ImGui::TextUnformatted(T(TKEY("use_markers_to_optimize_tooltip_1"), "Uses frame markers for tighter Reflex timing.")); + ImGui::TextUnformatted(T(TKEY("use_markers_to_optimize_tooltip_2"), "Try On first; turn Off if it causes stutter on your setup.")); } if (!markerOptimizationAvailable) ImGui::EndDisabled(); if (!markerOptimizationAvailable) { - ImGui::TextDisabled("Marker optimization unavailable (PCL not loaded)."); + ImGui::TextDisabled("%s", T(TKEY("marker_optimization_unavailable"), "Marker optimization unavailable (PCL not loaded).")); } - ImGui::Checkbox("Use FPS Limit", &settings.reflexUseFPSLimit); + ImGui::Checkbox(T(TKEY("use_fps_limit"), "Use FPS Limit"), &settings.reflexUseFPSLimit); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Uses Reflex's internal FPS cap for steadier frametimes."); - ImGui::TextUnformatted("Can lower latency versus uncapped rendering."); + ImGui::TextUnformatted(T(TKEY("use_fps_limit_tooltip_1"), "Uses Reflex's internal FPS cap for steadier frametimes.")); + ImGui::TextUnformatted(T(TKEY("use_fps_limit_tooltip_2"), "Can lower latency versus uncapped rendering.")); } if (!settings.reflexLowLatencyMode) @@ -394,10 +424,10 @@ void Upscaling::DrawSettings() if (!std::isfinite(settings.reflexFPSLimit)) settings.reflexFPSLimit = 60.0f; settings.reflexFPSLimit = std::clamp(settings.reflexFPSLimit, 20.0f, 240.0f); - ImGui::SliderFloat("FPS Limit", &settings.reflexFPSLimit, 20.0f, 240.0f, "%.0f"); + ImGui::SliderFloat(T(TKEY("fps_limit"), "FPS Limit"), &settings.reflexFPSLimit, 20.0f, 240.0f, "%.0f"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Set your frame cap target."); - ImGui::TextUnformatted("Start about 2-3 FPS below refresh rate (e.g. 117 for 120 Hz)."); + ImGui::TextUnformatted(T(TKEY("fps_limit_tooltip_1"), "Set your frame cap target.")); + ImGui::TextUnformatted(T(TKEY("fps_limit_tooltip_2"), "Start about 2-3 FPS below refresh rate (e.g. 117 for 120 Hz).")); } if (!settings.reflexUseFPSLimit) @@ -409,25 +439,25 @@ void Upscaling::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Backend Diagnostics")) { + if (ImGui::TreeNodeEx(T(TKEY("backend_diagnostics"), "Backend Diagnostics"))) { // Streamline log level selection const char* logLevels[] = { "Off", "Default", "Verbose" }; int logLevelIdx = static_cast(settings.streamlineLogLevel); - if (ImGui::Combo("Streamline Logging", &logLevelIdx, logLevels, IM_ARRAYSIZE(logLevels))) { + if (ImGui::Combo(T(TKEY("streamline_logging"), "Streamline Logging"), &logLevelIdx, logLevels, IM_ARRAYSIZE(logLevels))) { settings.streamlineLogLevel = static_cast(logLevelIdx); } - ImGui::TextUnformatted("Changing this requires a restart to take effect."); + ImGui::TextUnformatted(T(TKEY("streamline_logging_restart_note"), "Changing this requires a restart to take effect.")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G."); + ImGui::Text("%s", T(TKEY("streamline_logging_tooltip"), "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.")); } // VR Debug visualization -- per-eye buffers and native inputs if (globals::game::isVR) { ImGui::Separator(); static float debugRescale = 0.15f; - ImGui::SliderFloat("View Resize", &debugRescale, 0.05f, 1.f); + ImGui::SliderFloat(T(TKEY("view_resize"), "View Resize"), &debugRescale, 0.05f, 1.f); - if (ImGui::TreeNode("Upscaling Intermediates")) { + if (ImGui::TreeNode(T(TKEY("upscaling_intermediates"), "Upscaling Intermediates"))) { if (vrIntermediateMotionVectors[0]) { bool isDLSS = GetUpscaleMethod() == UpscaleMethod::kDLSS; if (vrIntermediateColorIn[0] && vrIntermediateColorOut[0]) { @@ -446,12 +476,12 @@ void Upscaling::DrawSettings() BUFFER_VIEWER_NODE_TITLE(vrIntermediateTransparencyMask[1], "Right Eye Transparency", debugRescale) } } else { - ImGui::TextDisabled("VR intermediates not yet created (enter game world)"); + ImGui::TextDisabled("%s", T(TKEY("vr_intermediates_not_created"), "VR intermediates not yet created (enter game world)")); } ImGui::TreePop(); } - if (ImGui::TreeNode("Native Inputs")) { + if (ImGui::TreeNode(T(TKEY("native_inputs"), "Native Inputs"))) { auto renderer = globals::game::renderer; auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; auto& mvec = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; @@ -634,6 +664,8 @@ void Upscaling::PostPostLoad() logger::info("[Upscaling] Installed hooks"); } +#undef I18N_KEY_PREFIX + Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const { if (streamline.featureDLSS) diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 2cc4b1297f..52f98ca8e1 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -23,6 +23,7 @@ struct Upscaling : Feature public: // Feature interface virtual inline std::string GetName() override { return "Upscaling"; } + virtual std::string GetDisplayName() override { return T("feature.upscaling.name", "Upscaling"); } virtual inline std::string GetShortName() override { return "Upscaling"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline bool SupportsVR() override { return true; } @@ -31,14 +32,12 @@ struct Upscaling : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Advanced upscaling and frame generation technologies for improved performance", - { "DLSS (Deep Learning Super Sampling) support", - "FSR (FidelityFX Super Resolution) support", - "TAA (Temporal Anti-Aliasing) support", - "Frame generation for supported systems" } - }; - } + return { T("feature.upscaling.description", "Advanced upscaling and frame generation technologies for improved performance"), + { T("feature.upscaling.key_feature_1", "DLSS (Deep Learning Super Sampling) support"), + T("feature.upscaling.key_feature_2", "FSR (FidelityFX Super Resolution) support"), + T("feature.upscaling.key_feature_3", "TAA (Temporal Anti-Aliasing) support"), + T("feature.upscaling.key_feature_4", "Frame generation for supported systems") } }; + }; float2 jitter = { 0, 0 }; diff --git a/src/Features/VR.h b/src/Features/VR.h index 459ca8287b..1c63d30fff 100644 --- a/src/Features/VR.h +++ b/src/Features/VR.h @@ -96,19 +96,18 @@ struct VR : OverlayFeature //============================================================================= virtual inline std::string GetName() override { return "VR"; } + virtual std::string GetDisplayName() override { return T("feature.vr.name", "VR"); } virtual inline std::string GetShortName() override { return "VR"; } virtual std::pair> GetFeatureSummary() override { - return { - "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", - { "Depth buffer culling optimization for VR performance", - "In-scene overlay menu with HMD/Controller/Fixed World attach modes", - "VR controller input with customizable button mappings", - "Grip-to-drag overlay positioning with depth control", - "Configurable occlusion culling parameters", - "Enhanced VR compatibility with SteamVR and OpenComposite" } - }; - } + return { T("feature.vr.description", "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments."), + { T("feature.vr.key_feature_1", "Depth buffer culling optimization for VR performance"), + T("feature.vr.key_feature_2", "In-scene overlay menu with HMD/Controller/Fixed World attach modes"), + T("feature.vr.key_feature_3", "VR controller input with customizable button mappings"), + T("feature.vr.key_feature_4", "Grip-to-drag overlay positioning with depth control"), + T("feature.vr.key_feature_5", "Configurable occlusion culling parameters"), + T("feature.vr.key_feature_6", "Enhanced VR compatibility with SteamVR and OpenComposite") } }; + }; virtual inline std::string_view GetShaderDefineName() override { return "VR_STEREO_OPT"; } virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return stereoOpt.CanDispatchStencil() && (t == RE::BSShader::Type::Utility || t == RE::BSShader::Type::Lighting); } diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp index fd287fed39..cf886a2cbf 100644 --- a/src/Features/VRStereoOptimizations.cpp +++ b/src/Features/VRStereoOptimizations.cpp @@ -2,6 +2,8 @@ #include "ExtendedMaterials.h" #include "Globals.h" +#include "I18n/I18n.h" +#define I18N_KEY_PREFIX "feature.vr_stereo." #include "Menu.h" #include "State.h" #include "Utils/D3D.h" @@ -253,37 +255,37 @@ void VRStereoOptimizations::ClearPomOffsetTexture() void VRStereoOptimizations::DrawSettings() { - const char* modeNames[] = { "Off", "Enable" }; + const char* modeNames[] = { T("feature.vr_stereo.off", "Off"), T("feature.vr_stereo.enable", "Enable") }; int currentMode = static_cast(settings.stereoMode); - if (ImGui::Combo("Enable Stereo Reprojection", ¤tMode, modeNames, IM_ARRAYSIZE(modeNames))) + if (ImGui::Combo(T(TKEY("enable_stereo_reprojection"), "Enable Stereo Reprojection"), ¤tMode, modeNames, IM_ARRAYSIZE(modeNames))) settings.stereoMode = static_cast(currentMode); - Util::AddTooltip("Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame."); + Util::AddTooltip(T(TKEY("enable_stereo_reprojection_tooltip"), "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.")); if (globals::game::isVR && settings.stereoMode == StereoMode::Enable && !loaded) { const auto& themeSettings = Menu::GetSingleton()->GetTheme(); ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, - "Restart is required to enable VR stereo reprojection."); + "%s", T(TKEY("restart_required"), "Restart is required to enable VR stereo reprojection.")); } if (settings.stereoMode == StereoMode::Off) return; - ImGui::SliderFloat("Disocclusion Depth Threshold", &settings.disocclusionDepthThreshold, 0.001f, 0.1f, "%.4f"); + ImGui::SliderFloat(T(TKEY("disocclusion_depth_threshold"), "Disocclusion Depth Threshold"), &settings.disocclusionDepthThreshold, 0.001f, 0.1f, "%.4f"); - ImGui::SliderFloat("Forward Occlusion Scale", &settings.forwardOcclusionScale, 0.0f, 1.0f, "%.2f"); - Util::AddTooltip("Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled."); + ImGui::SliderFloat(T(TKEY("forward_occlusion_scale"), "Forward Occlusion Scale"), &settings.forwardOcclusionScale, 0.0f, 1.0f, "%.2f"); + Util::AddTooltip(T(TKEY("forward_occlusion_scale_tooltip"), "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.")); if (globals::state->IsDeveloperMode()) { - if (ImGui::TreeNode("Debug")) { - ImGui::SliderFloat("Full Blend Distance", &settings.fullBlendDistance, 0.0f, 10000.0f, "%.0f"); - Util::AddTooltip("Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled."); - - ImGui::SliderFloat("POM Depth Scale", &settings.pomDepthScale, 0.0f, 500.0f, "%.1f"); - Util::AddTooltip("Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth."); - ImGui::Checkbox("Skip Pixel Reprojection", &settings.debugSkipMerge); - ImGui::Checkbox("Full Blend Depth View", &settings.debugFullBlendDepth); - ImGui::Checkbox("Debug POM Depth", &settings.debugPOMDepth); + if (ImGui::TreeNode(T(TKEY("debug"), "Debug"))) { + ImGui::SliderFloat(T(TKEY("full_blend_distance"), "Full Blend Distance"), &settings.fullBlendDistance, 0.0f, 10000.0f, "%.0f"); + Util::AddTooltip(T(TKEY("full_blend_distance_tooltip"), "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.")); + + ImGui::SliderFloat(T(TKEY("pom_depth_scale"), "POM Depth Scale"), &settings.pomDepthScale, 0.0f, 500.0f, "%.1f"); + Util::AddTooltip(T(TKEY("pom_depth_scale_tooltip"), "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.")); + ImGui::Checkbox(T(TKEY("skip_pixel_reprojection"), "Skip Pixel Reprojection"), &settings.debugSkipMerge); + ImGui::Checkbox(T(TKEY("full_blend_depth_view"), "Full Blend Depth View"), &settings.debugFullBlendDepth); + ImGui::Checkbox(T(TKEY("debug_pom_depth"), "Debug POM Depth"), &settings.debugPOMDepth); if (settings.debugFullBlendDepth) - ImGui::TextColored(ImVec4(0, 1, 1, 1), " Cyan = full blend zone (closer = stronger tint)"); + ImGui::TextColored(ImVec4(0, 1, 1, 1), "%s", T(TKEY("full_blend_zone_hint"), " Cyan = full blend zone (closer = stronger tint)")); ImGui::Text("Stencil swaps this frame: %u", stencilSwapCount); ImGui::TreePop(); } @@ -616,3 +618,5 @@ void VRStereoOptimizations::DeactivateStencil() logger::trace("[VRStereoOptimizations] Frame: stencilSwapCount={}", stencilSwapCount); stencilActive = false; } + +#undef I18N_KEY_PREFIX diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index d52d725ee3..db9f646b10 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -1,9 +1,12 @@ #include "VolumetricLighting.h" +#include "I18n/I18n.h" #include "InteriorSun.h" #include "ShaderCache.h" #include "State.h" +#define I18N_KEY_PREFIX "feature.volumetric_lighting." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( VolumetricLighting::TextureSize, Width, @@ -21,13 +24,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void VolumetricLighting::DrawSettings() { - if (ImGui::Checkbox("Enable Volumetric Lighting in Exteriors", &settings.ExteriorEnabled)) + if (ImGui::Checkbox(T(TKEY("enable_exteriors"), "Enable Volumetric Lighting in Exteriors"), &settings.ExteriorEnabled)) SetupVL(); if (settings.ExteriorEnabled) DrawVolumetricLightingSettings(settings.ExteriorQuality, settings.ExteriorCustomSize, false, !inInterior); - if (ImGui::Checkbox("Enable Volumetric Lighting in Interiors", &settings.InteriorEnabled)) + if (ImGui::Checkbox(T(TKEY("enable_interiors"), "Enable Volumetric Lighting in Interiors"), &settings.InteriorEnabled)) SetupVL(); if (settings.InteriorEnabled) @@ -37,8 +40,14 @@ void VolumetricLighting::DrawSettings() void VolumetricLighting::DrawVolumetricLightingSettings(int32_t& quality, TextureSize& customSize, const bool isInterior, const bool inLocationType) { auto& [Width, Height, Depth] = FetchCurrentSizeInUnits(isInterior); - - if (ImGui::SliderInt(isInterior ? "Interior Quality" : "Exterior Quality", &quality, 0, static_cast(Quality::Count) - 1, QualityNames[quality])) { + const char* qualityNames[] = { + T(TKEY("quality_low"), "Low"), + T(TKEY("quality_medium"), "Medium"), + T(TKEY("quality_high"), "High"), + T(TKEY("quality_custom"), "Custom") + }; + + if (ImGui::SliderInt(isInterior ? T(TKEY("interior_quality"), "Interior Quality") : T(TKEY("exterior_quality"), "Exterior Quality"), &quality, 0, static_cast(Quality::Count) - 1, qualityNames[quality])) { if (inLocationType) SetupVL(); } @@ -47,19 +56,19 @@ void VolumetricLighting::DrawVolumetricLightingSettings(int32_t& quality, Textur if (!isCustomQuality) ImGui::BeginDisabled(); - if (ImGui::SliderInt(isInterior ? "Interior Width" : "Exterior Width", &Width, 1, 20, FromUnits(Width, 32), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { + if (ImGui::SliderInt(isInterior ? T(TKEY("interior_width"), "Interior Width") : T(TKEY("exterior_width"), "Exterior Width"), &Width, 1, 20, FromUnits(Width, 32), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { customSize.Width = Width * 32; if (inLocationType) SetupVL(); } - if (ImGui::SliderInt(isInterior ? "Interior Height" : "Exterior Height", &Height, 1, 20, FromUnits(Height, 32), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { + if (ImGui::SliderInt(isInterior ? T(TKEY("interior_height"), "Interior Height") : T(TKEY("exterior_height"), "Exterior Height"), &Height, 1, 20, FromUnits(Height, 32), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { customSize.Height = Height * 32; if (inLocationType) SetupVL(); } - if (ImGui::SliderInt(isInterior ? "Interior Depth" : "Exterior Depth", &Depth, 1, 64, FromUnits(Depth, 10), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { + if (ImGui::SliderInt(isInterior ? T(TKEY("interior_depth"), "Interior Depth") : T(TKEY("exterior_depth"), "Exterior Depth"), &Depth, 1, 64, FromUnits(Depth, 10), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { customSize.Depth = Depth * 10; if (inLocationType) SetupVL(); @@ -337,4 +346,6 @@ void VolumetricLighting::CopyResource::thunk(ID3D11DeviceContext* a_this, ID3D11 if (!(Util::IsDynamicResolution() && singleton.bEnableVolumetricLighting)) { a_this->CopyResource(a_renderTarget, a_renderTargetSource); } -} \ No newline at end of file +} + +#undef I18N_KEY_PREFIX diff --git a/src/Features/VolumetricLighting.h b/src/Features/VolumetricLighting.h index e5251c52ef..a6bcf21c95 100644 --- a/src/Features/VolumetricLighting.h +++ b/src/Features/VolumetricLighting.h @@ -25,21 +25,19 @@ struct VolumetricLighting : Feature bool enabledAtBoot = false; virtual inline std::string GetName() override { return "Volumetric Lighting"; } + virtual std::string GetDisplayName() override { return T("feature.volumetric_lighting.name", "Volumetric Lighting"); } virtual inline std::string GetShortName() override { return "VolumetricLighting"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } virtual std::pair> GetFeatureSummary() override { - return { - "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\n" - "This adds dramatic god rays and atmospheric depth to both interior and exterior environments.", - { "Realistic light scattering", - "God rays and atmospheric effects", - "Separate interior/exterior settings", - "Configurable quality levels", - "Enhanced atmospheric immersion" } - }; - } + return { T("feature.volumetric_lighting.description", "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\nThis adds dramatic god rays and atmospheric depth to both interior and exterior environments."), + { T("feature.volumetric_lighting.key_feature_1", "Realistic light scattering"), + T("feature.volumetric_lighting.key_feature_2", "God rays and atmospheric effects"), + T("feature.volumetric_lighting.key_feature_3", "Separate interior/exterior settings"), + T("feature.volumetric_lighting.key_feature_4", "Configurable quality levels"), + T("feature.volumetric_lighting.key_feature_5", "Enhanced atmospheric immersion") } }; + }; virtual void SaveSettings(json&) override; virtual void LoadSettings(json&) override; diff --git a/src/Features/VolumetricShadows.h b/src/Features/VolumetricShadows.h index fb596dad6d..e7d5a385eb 100644 --- a/src/Features/VolumetricShadows.h +++ b/src/Features/VolumetricShadows.h @@ -6,6 +6,7 @@ struct VolumetricShadows : Feature { public: virtual inline std::string GetName() override { return "Volumetric Shadows"; } + virtual std::string GetDisplayName() override { return T("feature.volumetric_shadows.name", "Volumetric Shadows"); } virtual inline std::string GetShortName() override { return "VolumetricShadows"; } virtual inline std::string_view GetShaderDefineName() override { return "VOLUMETRIC_SHADOWS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLighting; } @@ -16,15 +17,12 @@ struct VolumetricShadows : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Volumetric Shadows provides downsampled VSM shadow maps for use by effects like particles and decals.\n" - "This improves shadow quality on transparent objects with minimal performance impact.", - { "Downsampled VSM shadows", - "Gaussian blur filtering", - "Multi-cascade support", - "Optimized for effects rendering" } - }; - } + return { T("feature.volumetric_shadows.description", "Volumetric Shadows provides downsampled VSM shadow maps for use by effects like particles and decals.\nThis improves shadow quality on transparent objects with minimal performance impact."), + { T("feature.volumetric_shadows.key_feature_1", "Downsampled VSM shadows"), + T("feature.volumetric_shadows.key_feature_2", "Gaussian blur filtering"), + T("feature.volumetric_shadows.key_feature_3", "Multi-cascade support"), + T("feature.volumetric_shadows.key_feature_4", "Optimized for effects rendering") } }; + }; bool HasShaderDefine(RE::BSShader::Type shaderType) override; diff --git a/src/Features/WaterEffects.h b/src/Features/WaterEffects.h index 08ef6bb70b..87a79d1f36 100644 --- a/src/Features/WaterEffects.h +++ b/src/Features/WaterEffects.h @@ -7,22 +7,20 @@ struct WaterEffects : Feature public: winrt::com_ptr causticsView; virtual inline std::string GetName() override { return "Water Effects"; } + virtual std::string GetDisplayName() override { return T("feature.water_effects.name", "Water Effects"); } virtual inline std::string GetShortName() override { return "WaterEffects"; } virtual inline std::string_view GetShaderDefineName() override { return "WATER_EFFECTS"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kWater; } virtual std::pair> GetFeatureSummary() override { - return { - "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\n" - "This feature adds dynamic light patterns and improved water visual quality.", - { "Realistic water caustics", - "Enhanced underwater lighting", - "Dynamic light patterns on water surfaces", - "Improved water visual fidelity", - "Atmospheric underwater effects" } - }; - } + return { T("feature.water_effects.description", "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\nThis feature adds dynamic light patterns and improved water visual quality."), + { T("feature.water_effects.key_feature_1", "Realistic water caustics"), + T("feature.water_effects.key_feature_2", "Enhanced underwater lighting"), + T("feature.water_effects.key_feature_3", "Dynamic light patterns on water surfaces"), + T("feature.water_effects.key_feature_4", "Improved water visual fidelity"), + T("feature.water_effects.key_feature_5", "Atmospheric underwater effects") } }; + }; bool HasShaderDefine(RE::BSShader::Type shaderType) override; diff --git a/src/Features/WetnessEffects.cpp b/src/Features/WetnessEffects.cpp index 15025d78ad..7cdca4fcfa 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -1,6 +1,9 @@ #include "WetnessEffects.h" -#include "Menu.h" #include "CSEditor.h" +#include "I18n/I18n.h" +#include "Menu.h" + +#define I18N_KEY_PREFIX "feature.wetness_effects." NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( WetnessEffects::Settings, @@ -125,7 +128,7 @@ static constexpr const char* MONSOON_DETAILED[] = { "Max precipitation: ~22 mm/hr (extreme)", "Multipliers: Wetness 2.0x, Puddle 2.5x, Transition 2.0x.", "Raindrop: 100% chance, grid 2.0 units, interval 0.2s.", - "Skryim light rain will not match wetness.", + "Skyrim light rain will not match wetness.", "Performance impact: High (may impact GPU)", nullptr }; @@ -190,6 +193,138 @@ static const std::array CLIMATE_PRESETS = { CLIMATE_PRESET_INFO[5].settings // Monsoon/Extreme } }; +static const char* GetClimatePresetDisplayName(size_t a_index) +{ + switch (a_index) { + case 0: + return T(TKEY("climate_preset_custom"), "Custom"); + case 1: + return T(TKEY("climate_preset_legacy"), "Legacy"); + case 2: + return T(TKEY("climate_preset_nordic"), "Nordic (Default)"); + case 3: + return T(TKEY("climate_preset_arctic"), "Arctic Tundra"); + case 4: + return T(TKEY("climate_preset_coastal"), "Temperate Coastal"); + case 5: + return T(TKEY("climate_preset_monsoon"), "Monsoon/Extreme"); + default: + return T(TKEY("climate_preset_unknown"), "Unknown"); + } +} + +static const char* GetClimatePresetShortDescription(size_t a_index) +{ + switch (a_index) { + case 0: + return T(TKEY("climate_preset_custom_desc"), "User-defined custom settings"); + case 1: + return T(TKEY("climate_preset_legacy_desc"), "Original rain effect values (very light)"); + case 2: + return T(TKEY("climate_preset_nordic_desc"), "Balanced Nordic climate (moderate rain)"); + case 3: + return T(TKEY("climate_preset_arctic_desc"), "Cold, dry Arctic climate (light rain)"); + case 4: + return T(TKEY("climate_preset_coastal_desc"), "Maritime climate (heavy rain)"); + case 5: + return T(TKEY("climate_preset_monsoon_desc"), "Extreme monsoon climate (extreme rain)"); + default: + return ""; + } +} + +static std::vector GetClimatePresetDetailedDescription(size_t a_index) +{ + switch (a_index) { + case 1: + return { + T(TKEY("climate_legacy_detail_0"), "Riverwood's original rain effect values for full backward compatibility."), + T(TKEY("climate_legacy_detail_1"), "Max precipitation: ~0.66 mm/hr (very light)"), + T(TKEY("climate_legacy_detail_2"), "Multipliers: Wetness 1.0x, Puddle 1.0x, Transition 1.0x."), + T(TKEY("climate_legacy_detail_3"), "Raindrop: 30% chance, grid 4.0 units, interval 0.5s."), + T(TKEY("climate_legacy_detail_4"), "Performance impact: Minimal (baseline)") + }; + case 2: + return { + T(TKEY("climate_nordic_detail_0"), "Balanced temperate Nordic climate."), + T(TKEY("climate_nordic_detail_1"), "Max precipitation: ~3.35 mm/hr (moderate)"), + T(TKEY("climate_nordic_detail_2"), "Multipliers: Wetness 1.0x, Puddle 1.0x, Transition 1.0x."), + T(TKEY("climate_nordic_detail_3"), "Raindrop: 100% chance, grid 3.0 units, interval 1.0s."), + T(TKEY("climate_nordic_detail_4"), "Performance impact: Low") + }; + case 3: + return { + T(TKEY("climate_arctic_detail_0"), "Cold, dry climate with minimal precipitation."), + T(TKEY("climate_arctic_detail_1"), "Max precipitation: ~1.08 mm/hr (light)"), + T(TKEY("climate_arctic_detail_2"), "Multipliers: Wetness 0.5x, Puddle 0.3x, Transition 0.5x."), + T(TKEY("climate_arctic_detail_3"), "Raindrop: 30% chance, grid 3.5 units, interval 0.4s."), + T(TKEY("climate_arctic_detail_4"), "Performance impact: Minimal") + }; + case 4: + return { + T(TKEY("climate_coastal_detail_0"), "Maritime climate with frequent, heavy precipitation."), + T(TKEY("climate_coastal_detail_1"), "Max precipitation: ~8.06 mm/hr (heavy)"), + T(TKEY("climate_coastal_detail_2"), "Multipliers: Wetness 1.5x, Puddle 1.7x, Transition 1.7x."), + T(TKEY("climate_coastal_detail_3"), "Raindrop: 80% chance, grid 2.5 units, interval 0.25s."), + T(TKEY("climate_coastal_detail_4"), "Performance impact: Moderate") + }; + case 5: + return { + T(TKEY("climate_monsoon_detail_0"), "Tropical/monsoon climate with extreme precipitation."), + T(TKEY("climate_monsoon_detail_1"), "Max precipitation: ~22 mm/hr (extreme)"), + T(TKEY("climate_monsoon_detail_2"), "Multipliers: Wetness 2.0x, Puddle 2.5x, Transition 2.0x."), + T(TKEY("climate_monsoon_detail_3"), "Raindrop: 100% chance, grid 2.0 units, interval 0.2s."), + T(TKEY("climate_monsoon_detail_4"), "Skyrim light rain will not match wetness."), + T(TKEY("climate_monsoon_detail_5"), "Performance impact: High (may impact GPU)") + }; + default: + return {}; + } +} + +static std::vector GetClimatePresetEffectDescription(size_t a_index) +{ + switch (a_index) { + case 1: + return { + T(TKEY("climate_legacy_effect_0"), "Original wetness accumulation (1.0x)"), + T(TKEY("climate_legacy_effect_1"), "Original puddle formation (1.0x)"), + T(TKEY("climate_legacy_effect_2"), "Original weather transitions (1.0x)"), + T(TKEY("climate_legacy_effect_3"), "Original raindrop frequency (1.0x)") + }; + case 2: + return { + T(TKEY("climate_nordic_effect_0"), "Standard wetness accumulation (1.0x)"), + T(TKEY("climate_nordic_effect_1"), "Standard puddle formation (1.0x)"), + T(TKEY("climate_nordic_effect_2"), "Standard weather transitions (1.0x)"), + T(TKEY("climate_nordic_effect_3"), "Moderate raindrop frequency (100% chance)") + }; + case 3: + return { + T(TKEY("climate_arctic_effect_0"), "Slow wetness accumulation (0.5x)"), + T(TKEY("climate_arctic_effect_1"), "Minimal puddle formation (0.3x)"), + T(TKEY("climate_arctic_effect_2"), "Slow weather transitions (0.5x)"), + T(TKEY("climate_arctic_effect_3"), "Sparse precipitation (30% chance)") + }; + case 4: + return { + T(TKEY("climate_coastal_effect_0"), "Fast wetness accumulation (1.5x)"), + T(TKEY("climate_coastal_effect_1"), "Enhanced puddle formation (1.7x)"), + T(TKEY("climate_coastal_effect_2"), "Rapid weather transitions (1.7x)"), + T(TKEY("climate_coastal_effect_3"), "Frequent rain events (80% chance)") + }; + case 5: + return { + T(TKEY("climate_monsoon_effect_0"), "Rapid wetness accumulation (2.0x)"), + T(TKEY("climate_monsoon_effect_1"), "Maximum puddle formation (2.5x)"), + T(TKEY("climate_monsoon_effect_2"), "Very dynamic weather (2.0x)"), + T(TKEY("climate_monsoon_effect_3"), "Maximum raindrop frequency (100% chance)") + }; + default: + return {}; + } +} + // Ripples code borrowed from po3 SplashesofStorms // https://github.com/powerof3/SplashesOfStorms/blob/master/src/Hooks.cpp under MIT License namespace Ripples @@ -252,7 +387,7 @@ void WetnessEffects::PostPostLoad() void WetnessEffects::DrawSettings() { // Climate Preset Selection - Always visible at the top - Util::DrawSectionHeader("Climate Presets", false, false); + Util::DrawSectionHeader(T(TKEY("climate_presets"), "Climate Presets"), false, false); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.3f, 0.4f, 0.6f)); // Subtle blue background ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.35f, 0.45f, 0.8f)); // Slightly darker for button @@ -260,12 +395,12 @@ void WetnessEffects::DrawSettings() // Extract names for combo box const char* presetNames[CLIMATE_PRESET_INFO.size()]; for (size_t i = 0; i < CLIMATE_PRESET_INFO.size(); ++i) { - presetNames[i] = CLIMATE_PRESET_INFO[i].name; + presetNames[i] = GetClimatePresetDisplayName(i); } // Map preset enum to combo index (Custom=0, Legacy=1, Nordic=2, Arctic=3, Coastal=4, Monsoon=5) int currentComboIndex = static_cast(climatePreset); - if (ImGui::Combo("Climate Preset", ¤tComboIndex, presetNames, static_cast(CLIMATE_PRESET_INFO.size()))) { // Map combo index back to preset enum + if (ImGui::Combo(T(TKEY("climate_preset"), "Climate Preset"), ¤tComboIndex, presetNames, static_cast(CLIMATE_PRESET_INFO.size()))) { // Map combo index back to preset enum // Simplified: map combo index directly to enum, with bounds check ClimatePreset newPreset = (currentComboIndex >= 0 && currentComboIndex < static_cast(CLIMATE_PRESET_INFO.size())) ? static_cast(currentComboIndex) : defaultPreset; @@ -281,24 +416,22 @@ void WetnessEffects::DrawSettings() ImGui::PopStyleColor(2); // Pop both style colors if (auto _tt = Util::HoverTooltipWrapper()) { if (currentComboIndex >= 0 && currentComboIndex < static_cast(CLIMATE_PRESET_INFO.size())) { - const auto& info = CLIMATE_PRESET_INFO[currentComboIndex]; - // Handle Custom preset differently if (currentComboIndex == 0) { // Custom preset - Util::DrawMultiLineTooltip({ "Custom settings - you have modified the preset values.", - "Select a preset above to apply predefined climate settings." }); + Util::DrawMultiLineTooltip({ T(TKEY("custom_preset_tooltip_0"), "Custom settings - you have modified the preset values."), + T(TKEY("custom_preset_tooltip_1"), "Select a preset above to apply predefined climate settings.") }); } else { // Build combined description lines for actual presets std::vector tooltipLines; - tooltipLines.push_back(info.shortDescription); + tooltipLines.push_back(GetClimatePresetShortDescription(static_cast(currentComboIndex))); // Add detailed description - for (const char* const* line = info.detailedDescription; *line != nullptr; ++line) { - tooltipLines.push_back(*line); + for (const char* line : GetClimatePresetDetailedDescription(static_cast(currentComboIndex))) { + tooltipLines.push_back(line); } - tooltipLines.push_back("Effects:"); + tooltipLines.push_back(T(TKEY("effects"), "Effects:")); // Add effect descriptions - for (const char* const* effect = info.effectDescription; *effect != nullptr; ++effect) { - tooltipLines.push_back(*effect); + for (const char* effect : GetClimatePresetEffectDescription(static_cast(currentComboIndex))) { + tooltipLines.push_back(effect); } std::vector tooltipLinesStr; @@ -315,106 +448,107 @@ void WetnessEffects::DrawSettings() ImGui::Separator(); ImGui::Spacing(); - if (ImGui::TreeNodeEx("Wetness Effects", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Enable Wetness", (bool*)&settings.EnableWetnessEffects)) { + if (ImGui::TreeNodeEx(T(TKEY("wetness_effects"), "Wetness Effects"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::Checkbox(T(TKEY("enable_wetness"), "Enable Wetness"), (bool*)&settings.EnableWetnessEffects)) { Ripples::UpdateSettings(); // Update cache when settings change } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables a wetness effect near water and when it is raining."); + ImGui::Text("%s", T(TKEY("enable_wetness_tooltip"), "Enables a wetness effect near water and when it is raining.")); } - ImGui::SliderFloat("Rain Wetness", &settings.MaxRainWetness, 0.0f, 2.5f); + ImGui::SliderFloat(T(TKEY("rain_wetness"), "Rain Wetness"), &settings.MaxRainWetness, 0.0f, 2.5f); if (ImGui::IsItemDeactivatedAfterEdit()) DetectCurrentPreset(); - ImGui::SliderFloat("Puddle Wetness", &settings.MaxPuddleWetness, 0.0f, 6.0f); + ImGui::SliderFloat(T(TKEY("puddle_wetness"), "Puddle Wetness"), &settings.MaxPuddleWetness, 0.0f, 6.0f); if (ImGui::IsItemDeactivatedAfterEdit()) DetectCurrentPreset(); - ImGui::SliderFloat("Shore Wetness", &settings.MaxShoreWetness, 0.0f, 1.0f); + ImGui::SliderFloat(T(TKEY("shore_wetness"), "Shore Wetness"), &settings.MaxShoreWetness, 0.0f, 1.0f); ImGui::TreePop(); } ImGui::Spacing(); ImGui::Spacing(); - if (ImGui::TreeNodeEx("Raindrop Effects", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Raindrop Effects", (bool*)&settings.EnableRaindropFx); + if (ImGui::TreeNodeEx(T(TKEY("raindrop_effects"), "Raindrop Effects"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_raindrop_effects"), "Enable Raindrop Effects"), (bool*)&settings.EnableRaindropFx); ImGui::BeginDisabled(!settings.EnableRaindropFx); - ImGui::Checkbox("Enable Splashes", (bool*)&settings.EnableSplashes); + ImGui::Checkbox(T(TKEY("enable_splashes"), "Enable Splashes"), (bool*)&settings.EnableSplashes); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Enables small splashes of wetness on dry surfaces."); - ImGui::Checkbox("Enable Ripples", (bool*)&settings.EnableRipples); + ImGui::Text("%s", T(TKEY("enable_splashes_tooltip"), "Enables small splashes of wetness on dry surfaces.")); + ImGui::Checkbox(T(TKEY("enable_ripples"), "Enable Ripples"), (bool*)&settings.EnableRipples); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Enables circular ripples on puddles, and to a less extent other wet surfaces"); + ImGui::Text("%s", T(TKEY("enable_ripples_tooltip"), "Enables circular ripples on puddles, and to a less extent other wet surfaces")); ImGui::BeginDisabled(splashesOfStormsLoaded); - std::string checkboxLabel = splashesOfStormsLoaded ? - "Enable Vanilla Ripples - Controlled by Splashes of Storms" : - "Enable Vanilla Ripples"; + const char* checkboxLabel = splashesOfStormsLoaded ? + T(TKEY("enable_vanilla_ripples_controlled"), "Enable Vanilla Ripples - Controlled by Splashes of Storms") : + T(TKEY("enable_vanilla_ripples"), "Enable Vanilla Ripples"); - if (ImGui::Checkbox(checkboxLabel.c_str(), (bool*)&settings.EnableVanillaRipples)) { + if (ImGui::Checkbox(checkboxLabel, (bool*)&settings.EnableVanillaRipples)) { Ripples::UpdateSettings(); // Update cache when settings change } if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Enables default ripples (e.g., Ripples01).", - "Disabling may not take effect until the next weather change." }); + Util::DrawMultiLineTooltip({ T(TKEY("vanilla_ripples_tooltip_0"), "Enables default ripples (e.g., Ripples01)."), + T(TKEY("vanilla_ripples_tooltip_1"), "Disabling may not take effect until the next weather change.") }); } ImGui::EndDisabled(); - ImGui::SliderFloat("Effect Range", &settings.RaindropFxRange, 1e2f, 2e3f, "%.0f units"); + ImGui::SliderFloat(T(TKEY("effect_range"), "Effect Range"), &settings.RaindropFxRange, 1e2f, 2e3f, "%.0f units"); if (auto _tt = Util::HoverTooltipWrapper()) { + auto meters = Util::Units::GameUnitsToMeters(settings.RaindropFxRange); std::vector tooltipLines = { - "Range for raindrop effects", + T(TKEY("effect_range_tooltip"), "Range for raindrop effects"), Util::Units::FormatDistance(settings.RaindropFxRange), - std::format("{:.2f} meters", Util::Units::GameUnitsToMeters(settings.RaindropFxRange)) + std::vformat(T(TKEY("meters_format"), "{:.2f} meters"), std::make_format_args(meters)) }; Util::DrawMultiLineTooltip(tooltipLines); } - if (ImGui::TreeNodeEx("Raindrops")) { + if (ImGui::TreeNodeEx(T(TKEY("raindrops"), "Raindrops"))) { ImGui::BulletText( - "At every interval, a raindrop is placed within each grid cell.\n" - "Only a set portion of raindrops will actually trigger splashes and ripples.\n"); + "%s", + T(TKEY("raindrops_help"), "At every interval, a raindrop is placed within each grid cell.\nOnly a set portion of raindrops will actually trigger splashes and ripples.\n")); - ImGui::SliderFloat("Grid Size", &settings.RaindropGridSize, 1.0f, 10.0f, "%.1f units"); + ImGui::SliderFloat(T(TKEY("grid_size"), "Grid Size"), &settings.RaindropGridSize, 1.0f, 10.0f, "%.1f units"); if (auto _tt = Util::HoverTooltipWrapper()) { std::vector tooltipLines = { - "Spatial grid size for raindrop placement (smaller = more grid cells, higher GPU cost)", - "This is the most performance-sensitive setting. Lower only if needed for realism.", + T(TKEY("grid_size_tooltip_0"), "Spatial grid size for raindrop placement (smaller = more grid cells, higher GPU cost)"), + T(TKEY("grid_size_tooltip_1"), "This is the most performance-sensitive setting. Lower only if needed for realism."), Util::Units::FormatDistance(settings.RaindropGridSize) }; Util::DrawMultiLineTooltip(tooltipLines); } - ImGui::SliderFloat("Interval", &settings.RaindropInterval, 0.1f, 2.0f, "%.1f sec"); + ImGui::SliderFloat(T(TKEY("interval"), "Interval"), &settings.RaindropInterval, 0.1f, 2.0f, "%.1f sec"); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How often raindrop effects are checked (lower = more frequent, moderate performance impact)"); + ImGui::Text("%s", T(TKEY("interval_tooltip"), "How often raindrop effects are checked (lower = more frequent, moderate performance impact)")); } - ImGui::SliderFloat("Chance", &settings.RaindropChance, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("chance"), "Chance"), &settings.RaindropChance, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Portion of raindrops that will actually cause splashes and ripples. Higher values increase effect density but have the least performance impact."); + ImGui::Text("%s", T(TKEY("chance_tooltip"), "Portion of raindrops that will actually cause splashes and ripples. Higher values increase effect density but have the least performance impact.")); } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Splashes")) { - ImGui::SliderFloat("Strength", &settings.SplashesStrength, 0.f, 2.f, "%.2f"); - ImGui::SliderFloat("Min Radius", &settings.SplashesMinRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::TreeNodeEx(T(TKEY("splashes"), "Splashes"))) { + ImGui::SliderFloat(T(TKEY("strength"), "Strength"), &settings.SplashesStrength, 0.f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("min_radius"), "Min Radius"), &settings.SplashesMinRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("As portion of grid size."); - ImGui::SliderFloat("Max Radius", &settings.SplashesMaxRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + ImGui::Text("%s", T(TKEY("portion_of_grid_size"), "As portion of grid size.")); + ImGui::SliderFloat(T(TKEY("max_radius"), "Max Radius"), &settings.SplashesMaxRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("As portion of grid size."); - ImGui::SliderFloat("Lifetime", &settings.SplashesLifetime, 0.1f, 20.f, "%.1f"); + ImGui::Text("%s", T(TKEY("portion_of_grid_size"), "As portion of grid size.")); + ImGui::SliderFloat(T(TKEY("lifetime"), "Lifetime"), &settings.SplashesLifetime, 0.1f, 20.f, "%.1f"); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Ripples")) { - ImGui::SliderFloat("Strength", &settings.RippleStrength, 0.f, 2.f, "%.2f"); - ImGui::SliderFloat("Radius", &settings.RippleRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::TreeNodeEx(T(TKEY("ripples"), "Ripples"))) { + ImGui::SliderFloat(T(TKEY("strength"), "Strength"), &settings.RippleStrength, 0.f, 2.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("radius"), "Radius"), &settings.RippleRadius, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("As portion of grid size."); - ImGui::SliderFloat("Breadth", &settings.RippleBreadth, 0.f, 1.f, "%.2f"); - ImGui::SliderFloat("Lifetime", &settings.RippleLifetime, 0.f, settings.RaindropInterval, "%.2f sec", ImGuiSliderFlags_AlwaysClamp); + ImGui::Text("%s", T(TKEY("portion_of_grid_size"), "As portion of grid size.")); + ImGui::SliderFloat(T(TKEY("breadth"), "Breadth"), &settings.RippleBreadth, 0.f, 1.f, "%.2f"); + ImGui::SliderFloat(T(TKEY("lifetime"), "Lifetime"), &settings.RippleLifetime, 0.f, settings.RaindropInterval, "%.2f sec", ImGuiSliderFlags_AlwaysClamp); ImGui::TreePop(); } @@ -426,50 +560,52 @@ void WetnessEffects::DrawSettings() ImGui::Spacing(); ImGui::Spacing(); - if (ImGui::TreeNodeEx("Advanced", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Weather transition speed", &settings.WeatherTransitionSpeed, 0.2f, 8.0f); + if (ImGui::TreeNodeEx(T(TKEY("advanced"), "Advanced"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("weather_transition_speed"), "Weather transition speed"), &settings.WeatherTransitionSpeed, 0.2f, 8.0f); if (ImGui::IsItemDeactivatedAfterEdit()) DetectCurrentPreset(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How fast wetness appears when raining and how quickly it dries after rain has stopped."); + ImGui::Text("%s", T(TKEY("weather_transition_speed_tooltip"), "How fast wetness appears when raining and how quickly it dries after rain has stopped.")); } - ImGui::SliderFloat("Min Rain Wetness", &settings.MinRainWetness, 0.0f, 0.9f); + ImGui::SliderFloat(T(TKEY("min_rain_wetness"), "Min Rain Wetness"), &settings.MinRainWetness, 0.0f, 0.9f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The minimum amount an object gets wet from rain."); + ImGui::Text("%s", T(TKEY("min_rain_wetness_tooltip"), "The minimum amount an object gets wet from rain.")); } - ImGui::SliderFloat("Skin Wetness", &settings.SkinWetness, 0.0f, 1.0f); + ImGui::SliderFloat(T(TKEY("skin_wetness"), "Skin Wetness"), &settings.SkinWetness, 0.0f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How wet character skin and hair get during rain."); + ImGui::Text("%s", T(TKEY("skin_wetness_tooltip"), "How wet character skin and hair get during rain.")); } - ImGui::SliderInt("Shore Range", (int*)&settings.ShoreRange, 1, 64); + ImGui::SliderInt(T(TKEY("shore_range"), "Shore Range"), (int*)&settings.ShoreRange, 1, 64); if (auto _tt = Util::HoverTooltipWrapper()) { + auto meters = Util::Units::GameUnitsToMeters(static_cast(settings.ShoreRange)); std::vector tooltipLines = { - "The maximum distance from a body of water that Shore Wetness affects", + T(TKEY("shore_range_tooltip"), "The maximum distance from a body of water that Shore Wetness affects"), Util::Units::FormatDistance(static_cast(settings.ShoreRange)), - std::format("{:.2f} meters", Util::Units::GameUnitsToMeters(static_cast(settings.ShoreRange))) + std::vformat(T(TKEY("meters_format"), "{:.2f} meters"), std::make_format_args(meters)) }; Util::DrawMultiLineTooltip(tooltipLines); } - ImGui::SliderFloat("Puddle Radius", &settings.PuddleRadius, 0.3f, 3.0f); + ImGui::SliderFloat(T(TKEY("puddle_radius"), "Puddle Radius"), &settings.PuddleRadius, 0.3f, 3.0f); if (auto _tt = Util::HoverTooltipWrapper()) { + auto puddleMeters = Util::Units::GameUnitsToMeters(settings.PuddleRadius); std::vector tooltipLines = { - "The radius used to determine puddle size and location", + T(TKEY("puddle_radius_tooltip"), "The radius used to determine puddle size and location"), Util::Units::FormatDistance(settings.PuddleRadius), - std::format("{:.2f} meters", Util::Units::GameUnitsToMeters(settings.PuddleRadius)) + std::vformat(T(TKEY("meters_format"), "{:.2f} meters"), std::make_format_args(puddleMeters)) }; Util::DrawMultiLineTooltip(tooltipLines); } - ImGui::SliderFloat("Puddle Max Angle", &settings.PuddleMaxAngle, 0.6f, 1.0f); + ImGui::SliderFloat(T(TKEY("puddle_max_angle"), "Puddle Max Angle"), &settings.PuddleMaxAngle, 0.6f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How flat a surface needs to be for puddles to form on it."); + ImGui::Text("%s", T(TKEY("puddle_max_angle_tooltip"), "How flat a surface needs to be for puddles to form on it.")); } - ImGui::SliderFloat("Puddle Min Wetness", &settings.PuddleMinWetness, 0.0f, 1.0f); + ImGui::SliderFloat(T(TKEY("puddle_min_wetness"), "Puddle Min Wetness"), &settings.PuddleMinWetness, 0.0f, 1.0f); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("The wetness value at which puddles start to form."); + ImGui::Text("%s", T(TKEY("puddle_min_wetness_tooltip"), "The wetness value at which puddles start to form.")); } ImGui::TreePop(); @@ -479,35 +615,36 @@ void WetnessEffects::DrawSettings() ImGui::Spacing(); auto& csEditor = globals::features::csEditor; if (csEditor.loaded) { - if (ImGui::SmallButton(("Open " + csEditor.GetName()).c_str())) { + std::string csEditorName = csEditor.GetName(); + if (ImGui::SmallButton(std::vformat(T(TKEY("open_feature"), "Open {}"), std::make_format_args(csEditorName)).c_str())) { // Navigate to the replacement feature in the menu Menu::GetSingleton()->SelectFeatureMenu(csEditor.GetShortName()); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Open the installed %s feature", csEditor.GetShortName().c_str()); + ImGui::Text(T(TKEY("open_installed_feature_tooltip"), "Open the installed %s feature"), csEditorName.c_str()); } } - if (ImGui::TreeNodeEx("Debug", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Wetness Override", &debugSettings.EnableWetnessOverride); - ImGui::Checkbox("Enable Puddle Override", &debugSettings.EnablePuddleOverride); - ImGui::Checkbox("Enable Rain Override", &debugSettings.EnableRainOverride); - ImGui::Checkbox("Enable Interior/Exterior Override", &debugSettings.EnableIntExOverride); + if (ImGui::TreeNodeEx(T(TKEY("debug"), "Debug"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox(T(TKEY("enable_wetness_override"), "Enable Wetness Override"), &debugSettings.EnableWetnessOverride); + ImGui::Checkbox(T(TKEY("enable_puddle_override"), "Enable Puddle Override"), &debugSettings.EnablePuddleOverride); + ImGui::Checkbox(T(TKEY("enable_rain_override"), "Enable Rain Override"), &debugSettings.EnableRainOverride); + ImGui::Checkbox(T(TKEY("enable_interior_exterior_override"), "Enable Interior/Exterior Override"), &debugSettings.EnableIntExOverride); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "If disabled, will only use the exterior value. "); + "%s", T(TKEY("interior_exterior_override_tooltip"), "If disabled, will only use the exterior value. ")); } if (debugSettings.EnableWetnessOverride) { - ImGui::SliderFloat2("Wetness In/Exterior", &debugSettings.WetnessOverride.x, 0.0f, 2.0f); + ImGui::SliderFloat2(T(TKEY("wetness_in_exterior"), "Wetness In/Exterior"), &debugSettings.WetnessOverride.x, 0.0f, 2.0f); } if (debugSettings.EnablePuddleOverride) { - ImGui::SliderFloat2("Puddle Wetness In/Exterior", &debugSettings.PuddleWetnessOverride.x, 0.0f, 2.0f); + ImGui::SliderFloat2(T(TKEY("puddle_wetness_in_exterior"), "Puddle Wetness In/Exterior"), &debugSettings.PuddleWetnessOverride.x, 0.0f, 2.0f); } if (debugSettings.EnableRainOverride) { - ImGui::SliderFloat2("Rain In/Exterior", &debugSettings.RainOverride.x, 0.0f, 1.0f); + ImGui::SliderFloat2(T(TKEY("rain_in_exterior"), "Rain In/Exterior"), &debugSettings.RainOverride.x, 0.0f, 1.0f); } ImGui::TreePop(); } @@ -980,4 +1117,4 @@ bool WetnessEffects::DoesCurrentSettingsMatchPreset(ClimatePreset preset) const std::abs(settings.MaxPuddleWetness - expectedMaxPuddleWetness) < tolerance && std::abs(settings.WeatherTransitionSpeed - expectedWeatherTransitionSpeed) < tolerance && std::abs(settings.RaindropChance - expectedRaindropChance) < tolerance); -} \ No newline at end of file +} diff --git a/src/Features/WetnessEffects.h b/src/Features/WetnessEffects.h index e01e0bcc31..ff4c28d4d2 100644 --- a/src/Features/WetnessEffects.h +++ b/src/Features/WetnessEffects.h @@ -9,6 +9,7 @@ struct WetnessEffects : Feature public: virtual inline std::string GetName() override { return "Wetness Effects"; } + virtual std::string GetDisplayName() override { return T("feature.wetness_effects.name", "Wetness Effects"); } virtual inline std::string GetShortName() override { return "WetnessEffects"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetShaderDefineName() override { return "WETNESS_EFFECTS"; } @@ -16,15 +17,13 @@ struct WetnessEffects : Feature virtual std::pair> GetFeatureSummary() override { - return { - "Adds realistic wetness effects including rain-based surface wetness, puddle formation, shore wetness, and dynamic raindrop effects for enhanced weather immersion.", - { "Dynamic surface wetness based on weather conditions", - "Realistic puddle formation and shore wetness effects", - "Animated raindrop effects with splashes and ripples", - "Configurable wetness intensity and weather transitions", - "Support for skin wetness and material-specific responses" } - }; - } + return { T("feature.wetness_effects.description", "Adds realistic wetness effects including rain-based surface wetness, puddle formation, shore wetness, and dynamic raindrop effects for enhanced weather immersion."), + { T("feature.wetness_effects.key_feature_1", "Dynamic surface wetness based on weather conditions"), + T("feature.wetness_effects.key_feature_2", "Realistic puddle formation and shore wetness effects"), + T("feature.wetness_effects.key_feature_3", "Animated raindrop effects with splashes and ripples"), + T("feature.wetness_effects.key_feature_4", "Configurable wetness intensity and weather transitions"), + T("feature.wetness_effects.key_feature_5", "Support for skin wetness and material-specific responses") } }; + }; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; diff --git a/src/I18n/I18n.cpp b/src/I18n/I18n.cpp new file mode 100644 index 0000000000..51dbbdc67c --- /dev/null +++ b/src/I18n/I18n.cpp @@ -0,0 +1,413 @@ +#include "I18n.h" + +#include +#include +#include + +#include "Utils/FileSystem.h" + +namespace +{ + /** Validates a locale code against a strict pattern to prevent path traversal. */ + bool IsValidLocaleCode(const std::string& locale) + { + // Allow: 2-3 letter language, optional underscore + 2-4 letter region + // Examples: "en", "zh_CN", "pt_BR", "ja", "kok_IN" + static const std::regex pattern(R"(^[a-zA-Z]{2,3}(_[a-zA-Z]{2,4})?$)"); + return std::regex_match(locale, pattern); + } +} + +void I18n::Init() +{ + std::unique_lock lock(mutex_); + + DiscoverLocales(); + + // Always load English as the fallback + if (!LoadLocaleInto("en", fallback_)) { + logger::info( + "[I18n] en.json not found or empty. " + "Inline defaults from T(key, default) will be used."); + } + + // Determine which locale to use. + // The saved locale preference is read from SettingsUser.json by State::Load() + // and forwarded here via SetLocale() before or after Init(). + // If currentLocale_ was already set (via an early SetLocale call), honour it. + if (currentLocale_ != "en") { + std::unordered_map loaded; + if (LoadLocaleInto(currentLocale_, loaded)) { + strings_ = std::move(loaded); + } else { + logger::warn("[I18n] Could not load locale '{}', falling back to English.", + currentLocale_); + currentLocale_ = "en"; + } + } + + logger::info("[I18n] Initialized. Locale: {} | {} available locale(s) | {} fallback keys", + currentLocale_, availableLocales_.size(), fallback_.size()); +} + +const char* I18n::Get(std::string_view key, const char* defaultText) const +{ + std::string keyStr(key); + + // Fast path: try under shared lock (concurrent readers OK) + { + std::shared_lock lock(mutex_); + + // 1. Try current locale + if (!strings_.empty()) { + auto it = strings_.find(keyStr); + if (it != strings_.end()) { + return it->second.c_str(); + } + } + + // 2. Try English fallback (from en.json) + { + auto it = fallback_.find(keyStr); + if (it != fallback_.end()) { + return it->second.c_str(); + } + } + + // 3. Check if already cached + { + auto it = defaultCache_.find(keyStr); + if (it != defaultCache_.end()) { + return it->second; + } + } + } + + // Slow path: need to insert into defaultCache_ — acquire exclusive lock + { + std::unique_lock lock(mutex_); + + // Double-check after acquiring exclusive lock (another thread may have inserted) + auto it = defaultCache_.find(keyStr); + if (it != defaultCache_.end()) { + return it->second; + } + + // Store string in deque (pointer-stable: deque never invalidates on push_back) + defaultStorage_.emplace_back(defaultText ? std::string(defaultText) : keyStr); + const char* ptr = defaultStorage_.back().c_str(); + defaultCache_.emplace(keyStr, ptr); + return ptr; + } +} + +std::string I18n::Format(std::string_view key, + const std::unordered_map& args, + const char* defaultText) const +{ + const char* raw = Get(key, defaultText); + return SubstitutePlaceholders(std::string(raw), args); +} + +std::string I18n::GetCurrentLocale() const +{ + std::shared_lock lock(mutex_); + return currentLocale_; +} + +void I18n::SetLocale(const std::string& locale) +{ + std::unique_lock lock(mutex_); + + if (locale == currentLocale_) + return; + + if (!IsValidLocaleCode(locale)) { + logger::warn("[I18n] Rejected invalid locale code: '{}'", locale); + return; + } + + if (locale == "en") { + // English uses fallback_ directly; no need for strings_ + strings_.clear(); + defaultCache_.clear(); + defaultStorage_.clear(); + currentLocale_ = "en"; + logger::info("[I18n] Switched to English (en)."); + return; + } + + std::unordered_map loaded; + if (LoadLocaleInto(locale, loaded)) { + strings_ = std::move(loaded); + defaultCache_.clear(); + defaultStorage_.clear(); + currentLocale_ = locale; + logger::info("[I18n] Switched to locale '{}'.", locale); + } else { + logger::warn("[I18n] Failed to load locale '{}'. Staying on '{}'.", + locale, currentLocale_); + } +} + +std::vector> I18n::GetAvailableLocales() const +{ + std::shared_lock lock(mutex_); + return availableLocales_; +} + +void I18n::Reload() +{ + std::unique_lock lock(mutex_); + + fallback_.clear(); + strings_.clear(); + defaultCache_.clear(); + defaultStorage_.clear(); + availableLocales_.clear(); + + DiscoverLocales(); + LoadLocaleInto("en", fallback_); + + if (currentLocale_ != "en") { + std::unordered_map loaded; + if (LoadLocaleInto(currentLocale_, loaded)) { + strings_ = std::move(loaded); + } else { + logger::warn( + "[I18n] Reload: could not load locale '{}', " + "falling back to English.", + currentLocale_); + currentLocale_ = "en"; + } + } + + logger::info("[I18n] Reloaded. Locale: {} | {} fallback keys", + currentLocale_, fallback_.size()); +} + +// ─── Private ───────────────────────────────────────────────────────────────── + +void I18n::DiscoverLocales() +{ + auto translationsPath = Util::PathHelpers::GetTranslationsPath(); + + if (!std::filesystem::exists(translationsPath)) { + logger::info("[I18n] Translations directory not found: {}", + translationsPath.string()); + // At minimum, register English as available + availableLocales_.emplace_back("en", "English"); + return; + } + + for (const auto& entry : std::filesystem::directory_iterator(translationsPath)) { + if (!entry.is_regular_file()) + continue; + auto ext = entry.path().extension().string(); + // Case-insensitive extension check + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (ext != ".json") + continue; + + auto locale = entry.path().stem().string(); + std::string displayName = locale; // default to code + + // Try to read _meta.language for a friendly display name + try { + std::ifstream f(entry.path()); + if (f.is_open()) { + nlohmann::json j; + f >> j; + if (j.contains("_meta") && j["_meta"].contains("language")) { + displayName = j["_meta"]["language"].get(); + } + } + } catch (const std::exception& e) { + logger::warn("[I18n] Error reading metadata from {}: {}", + entry.path().string(), e.what()); + } + + availableLocales_.emplace_back(locale, displayName); + } + + // Sort with "en" (English) first, then alphabetically by display name + std::sort(availableLocales_.begin(), availableLocales_.end(), + [](const auto& a, const auto& b) { + if (a.first == b.first) + return false; + if (a.first == "en") + return true; + if (b.first == "en") + return false; + if (a.second != b.second) + return a.second < b.second; + return a.first < b.first; + }); + + if (availableLocales_.empty()) { + availableLocales_.emplace_back("en", "English"); + } +} + +bool I18n::LoadLocaleInto(const std::string& locale, + std::unordered_map& target) const +{ + auto filePath = GetLocaleFilePath(locale); + + std::ifstream f(filePath); + if (!f.is_open()) { + logger::info("[I18n] Locale file not found: {}", filePath.string()); + return false; + } + + try { + nlohmann::json j; + f >> j; + + if (!j.is_object()) { + logger::warn("[I18n] Locale file is not a JSON object: {}", filePath.string()); + return false; + } + + size_t count = 0; + for (auto& [key, value] : j.items()) { + // Skip the metadata block + if (key == "_meta") + continue; + + if (value.is_string()) { + target[key] = value.get(); + ++count; + } + } + + logger::info("[I18n] Loaded {} keys from '{}'", count, filePath.string()); + return true; + } catch (const nlohmann::json::parse_error& e) { + logger::error("[I18n] JSON parse error in {}: {}", filePath.string(), e.what()); + return false; + } catch (const std::exception& e) { + logger::error("[I18n] Error loading {}: {}", filePath.string(), e.what()); + return false; + } +} + +std::filesystem::path I18n::GetLocaleFilePath(const std::string& locale) const +{ + return Util::PathHelpers::GetTranslationsPath() / (locale + ".json"); +} + +std::string I18n::SubstitutePlaceholders(const std::string& tmpl, + const std::unordered_map& args) +{ + if (args.empty()) + return tmpl; + + std::string result; + result.reserve(tmpl.size()); + + size_t i = 0; + while (i < tmpl.size()) { + if (tmpl[i] == '{') { + auto closePos = tmpl.find('}', i + 1); + if (closePos != std::string::npos) { + auto name = tmpl.substr(i + 1, closePos - i - 1); + auto it = args.find(name); + if (it != args.end()) { + result += it->second; + i = closePos + 1; + continue; + } + } + } + result += tmpl[i]; + ++i; + } + + return result; +} + +std::string I18n::DetectSystemLocale() const +{ + // Get the Windows UI language (LANGID = primary + sublanguage) + LANGID langId = GetUserDefaultUILanguage(); + WORD primary = PRIMARYLANGID(langId); + WORD sub = SUBLANGID(langId); + + // Map Windows LANG_* constants to our locale codes + std::string detected; + switch (primary) { + case LANG_CHINESE: + // Distinguish Simplified vs Traditional + if (sub == SUBLANG_CHINESE_SIMPLIFIED || sub == SUBLANG_CHINESE_SINGAPORE) { + detected = "zh_CN"; + } else { + detected = "zh_TW"; + } + break; + case LANG_JAPANESE: + detected = "ja"; + break; + case LANG_KOREAN: + detected = "ko"; + break; + case LANG_GERMAN: + detected = "de"; + break; + case LANG_FRENCH: + detected = "fr"; + break; + case LANG_SPANISH: + detected = "es"; + break; + case LANG_PORTUGUESE: + if (sub == SUBLANG_PORTUGUESE_BRAZILIAN) { + detected = "pt_BR"; + } else { + detected = "pt"; + } + break; + case LANG_RUSSIAN: + detected = "ru"; + break; + case LANG_ITALIAN: + detected = "it"; + break; + case LANG_POLISH: + detected = "pl"; + break; + case LANG_TURKISH: + detected = "tr"; + break; + case LANG_THAI: + detected = "th"; + break; + case LANG_VIETNAMESE: + detected = "vi"; + break; + case LANG_UKRAINIAN: + detected = "uk"; + break; + default: + detected = "en"; + break; + } + + // Check if the detected locale has a translation file available + std::shared_lock lock(mutex_); + for (const auto& [code, name] : availableLocales_) { + if (code == detected) { + return detected; + } + } + + // Try matching just the primary language prefix (e.g. "zh" matches "zh_CN") + std::string prefix = detected.substr(0, 2); + for (const auto& [code, name] : availableLocales_) { + if (code.starts_with(prefix)) { + return code; + } + } + + return "en"; +} diff --git a/src/I18n/I18n.h b/src/I18n/I18n.h new file mode 100644 index 0000000000..da4cc4c02f --- /dev/null +++ b/src/I18n/I18n.h @@ -0,0 +1,208 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Internationalization (i18n) engine for Community Shaders. + * + * Loads flat-JSON translation files from the Translations/ directory. + * Supports runtime language switching, named placeholder formatting ({name}), + * and automatic fallback to English when a key is missing in the current locale. + * + * ## Developer workflow + * + * Use the two-argument T() form everywhere in source code: + * + * ImGui::Text("%s", T("menu.home.welcome", "Welcome to Community Shaders")); + * + * The second argument is the English default. It serves as: + * 1. The fallback text when no translation file is loaded + * 2. The source for the extraction script (tools/extract-i18n.py) which + * auto-generates en.json from source code + * + * Developer only touches ONE file (the source code). en.json is auto-generated. + * Translators work from en.json on Weblate/GitHub and never touch source code. + * + * ## String ownership + * + * The internal maps own all std::string values, so Get() returns stable + * const char* pointers suitable for ImGui APIs as long as SetLocale() / + * Reload() is not called concurrently. + * + * ## Thread safety + * + * Init/SetLocale/Reload acquire an exclusive lock; + * Get/Format acquire a shared lock. Concurrent reads are safe. + */ +class I18n +{ +public: + static I18n* GetSingleton() + { + static I18n singleton; + return &singleton; + } + + /** + * Initialize the i18n system. + * Discovers available locales, loads the saved locale (or falls back to "en"), + * and populates the English fallback table. + */ + void Init(); + + /** + * Get a translated string by key, with an inline English default. + * + * Lookup order: + * 1. Current locale map + * 2. English fallback map (from en.json) + * 3. Inline default text (if provided) + * 4. The key itself (last resort) + * + * @param key Dot-notation key, e.g. "menu.home.welcome" + * @param defaultText English default text (used as fallback AND as source + * for the extraction script). Pass nullptr to skip. + * @return Pointer to a null-terminated string owned by this object. + * Valid until the next SetLocale() or Reload() call. + */ + const char* Get(std::string_view key, const char* defaultText = nullptr) const; + + /** + * Get a translated string with named placeholder substitution. + * Placeholders use {name} syntax, e.g. "Welcome {version}". + * + * @param key Translation key + * @param args Map of placeholder names to replacement values + * @param defaultText English default (optional, same role as in Get) + * @return Fully substituted string (caller owns the std::string) + */ + std::string Format(std::string_view key, + const std::unordered_map& args, + const char* defaultText = nullptr) const; + + /** @return Current locale code, e.g. "en", "zh_CN" */ + std::string GetCurrentLocale() const; + + /** + * Switch to a different locale at runtime. + * @param locale Locale code matching a JSON filename (without extension) + */ + void SetLocale(const std::string& locale); + + /** + * @return List of (locale_code, display_name) pairs for all discovered locales. + * Display names come from each file's _meta.language field. + */ + std::vector> GetAvailableLocales() const; + + /** Reload translation files from disk (useful during development). */ + void Reload(); + + /** + * Detect the system UI language and return the best matching available locale. + * Uses Windows GetUserDefaultUILanguage() to determine the system language, + * then matches against available translation files. + * + * @return Best matching locale code (e.g. "zh_CN", "ja", "de"), or "en" if no match. + */ + std::string DetectSystemLocale() const; + +private: + I18n() = default; + + /** Scan the Translations/ directory for *.json files and populate availableLocales_. */ + void DiscoverLocales(); + + /** + * Load all key-value pairs from a locale JSON file into the target map. + * Skips the "_meta" object. + * @param locale Locale code + * @param target Map to populate + * @return true if loaded successfully + */ + bool LoadLocaleInto(const std::string& locale, + std::unordered_map& target) const; + + /** Resolve the filesystem path for a given locale code. */ + std::filesystem::path GetLocaleFilePath(const std::string& locale) const; + + /** Perform {placeholder} substitution on a raw template string. */ + static std::string SubstitutePlaceholders(const std::string& tmpl, + const std::unordered_map& args); + + mutable std::shared_mutex mutex_; + + std::string currentLocale_ = "en"; + + // Current locale strings (may be empty for "en" — we just use fallback_) + std::unordered_map strings_; + + // English fallback (always loaded from en.json) + std::unordered_map fallback_; + + // (locale_code, display_name) — discovered from Translations/*.json + std::vector> availableLocales_; + + // Cache of inline defaults and missing keys — ensures pointer stability. + // Uses deque for string storage (deque never invalidates pointers on push_back) + // and unordered_map for O(1) lookup by key. + // Mutable because it's populated lazily from const Get(). + mutable std::deque defaultStorage_; + mutable std::unordered_map defaultCache_; +}; + +// ─── Convenience free function ─────────────────────────────────────────────── + +/** + * Get a translated string. Two usage patterns: + * + * // Preferred: inline default — developer only touches this one file + * T("menu.home.welcome", "Welcome to Community Shaders") + * + * // Key-only: falls back to en.json, then the key itself + * T("menu.home.welcome") + * + * The inline default serves double duty: + * 1. Runtime fallback when no translation file has the key + * 2. Source text for tools/extract-i18n.py to auto-generate en.json + */ +inline const char* T(std::string_view key, const char* defaultText = nullptr) +{ + return I18n::GetSingleton()->Get(key, defaultText); +} + +// ─── Scoped prefix macro ───────────────────────────────────────────────────── + +/** + * TKEY(suffix) — Compile-time key prefix concatenation. + * + * Define I18N_KEY_PREFIX at the top of a file, then use TKEY("suffix") + * to avoid repeating the full dotted path at every call site. + * + * Example (in GrassLighting.cpp): + * + * #define I18N_KEY_PREFIX "feature.grass_lighting." + * + * ImGui::SliderFloat(T(TKEY("glossiness"), "Glossiness"), ...); + * // → T("feature.grass_lighting.glossiness", "Glossiness") + * + * ImGui::Text("%s", T(TKEY("sss_tooltip"), + * "Subsurface Scattering amount.\n" + * "Models light transport through the surface.")); + * + * At end of file (to avoid prefix leaking to other translation units): + * + * #undef I18N_KEY_PREFIX + * + * C++ adjacent string literals ("a" "b" → "ab") do the concatenation + * at compile time — zero runtime cost. + */ +#define TKEY(suffix) I18N_KEY_PREFIX suffix diff --git a/src/Menu.cpp b/src/Menu.cpp index cde6f6fc5d..eecdca93da 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -24,6 +24,7 @@ #include "FeatureVersions.h" #include "Features/RenderDoc.h" #include "Features/Upscaling.h" +#include "I18n/I18n.h" #include "Menu/AdvancedSettingsRenderer.h" #include "Menu/BackgroundBlur.h" #include "Menu/FeatureListRenderer.h" @@ -39,13 +40,13 @@ #include "Util.h" #include "Utils/UI.h" +#include "CSEditor/EditorWindow.h" +#include "Features/CSEditor.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTestAggregator.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/ScreenshotFeature.h" #include "Features/VR.h" -#include "Features/CSEditor.h" -#include "CSEditor/EditorWindow.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -669,7 +670,7 @@ void Menu::DrawSettings() resetLayout = false; auto versionStr = Util::GetFormattedVersion(Plugin::VERSION); auto expectedTag = std::format("v{}", versionStr); - auto displayTitle = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Community Shaders {}", versionStr) : std::format("Community Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); + auto displayTitle = Plugin::BUILD_DESCRIBE == expectedTag ? I18n::GetSingleton()->Format("menu.window_title", { { "version", versionStr } }, "Community Shaders {version}") : I18n::GetSingleton()->Format("menu.window_title_dev", { { "version", versionStr }, { "build", std::string(Plugin::BUILD_DESCRIBE) } }, "Community Shaders {version} [{build}]"); // Use ### to keep a stable window ID regardless of build suffix, preserving docking state auto title = std::format("{}###CommunityShaders", displayTitle); @@ -785,14 +786,15 @@ void Menu::DrawDisableAtBootSettings() auto state = globals::state; auto& disabledFeatures = state->GetDisabledFeatures(); - ImGui::Text( - "Select features to disable at boot. " - "This is the same as deleting a feature.ini file. " - "Restart will be required to reenable."); + ImGui::Text("%s", + T("menu.disable_at_boot_desc", + "Select features to disable at boot. " + "This is the same as deleting a feature.ini file. " + "Restart will be required to reenable.")); ImGui::Spacing(); - if (ImGui::CollapsingHeader("Features", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader(T("menu.features", "Features"), ImGuiTreeNodeFlags_DefaultOpen)) { // Prepare a sorted list of feature pointers auto featureList = Feature::GetFeatureList(); std::sort(featureList.begin(), featureList.end(), [](Feature* a, Feature* b) { @@ -814,11 +816,21 @@ void Menu::DrawDisableAtBootSettings() void Menu::DrawFooter() { - ImGui::BulletText(std::format("Game Version: {} {}", magic_enum::enum_name(REL::Module::GetRuntime()), Util::GetFormattedVersion(REL::Module::get().version()).c_str()).c_str()); + ImGui::BulletText("%s", I18n::GetSingleton()->Format("menu.footer.game_version", + { { "runtime", std::string(magic_enum::enum_name(REL::Module::GetRuntime())) }, + { "version", Util::GetFormattedVersion(REL::Module::get().version()) } }, + "Game Version: {runtime} {version}") + .c_str()); ImGui::SameLine(); - ImGui::BulletText(std::format("D3D12 Swap Chain: {}", globals::features::upscaling.d3d12SwapChainActive ? "Active" : "Inactive").c_str()); + ImGui::BulletText("%s", I18n::GetSingleton()->Format("menu.footer.d3d12_swap_chain", + { { "status", globals::features::upscaling.d3d12SwapChainActive ? std::string(T("common.active", "Active")) : std::string(T("common.inactive", "Inactive")) } }, + "D3D12 Swap Chain: {status}") + .c_str()); ImGui::SameLine(); - ImGui::BulletText(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); + ImGui::BulletText("%s", I18n::GetSingleton()->Format("menu.footer.gpu", + { { "name", globals::state->adapterDescription } }, + "GPU: {name}") + .c_str()); } /** diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 8bd217b20d..203d2144e9 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -10,6 +10,7 @@ #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Fonts.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" @@ -23,7 +24,7 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( // Use TabBar system - tabs sorted alphabetically if (ImGui::BeginTabBar("##AdvancedSettingsTabs", ImGuiTabBarFlags_None)) { // Developer Tab - if (MenuFonts::BeginTabItemWithFont("Developer", Menu::FontRole::Subheading)) { + if (MenuFonts::BeginTabItemWithFont(T("menu.advanced.tab_developer", "Developer"), Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##DeveloperContent", ImVec2(0, 0), false)) { RenderDeveloperSection(); } @@ -32,7 +33,7 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } // Disable at Boot Tab - if (MenuFonts::BeginTabItemWithFont("Disable at Boot", Menu::FontRole::Subheading)) { + if (MenuFonts::BeginTabItemWithFont(T("menu.advanced.tab_disable_at_boot", "Disable at Boot"), Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##DisableAtBootContent", ImVec2(0, 0), false)) { RenderDisableAtBootSection(drawDisableAtBootSettings); } @@ -41,7 +42,7 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } // Logging Tab - if (MenuFonts::BeginTabItemWithFont("Logging", Menu::FontRole::Subheading)) { + if (MenuFonts::BeginTabItemWithFont(T("menu.advanced.tab_logging", "Logging"), Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##LoggingContent", ImVec2(0, 0), false)) { RenderLoggingSection(); } @@ -50,7 +51,7 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } // Shader Debug Tab - if (MenuFonts::BeginTabItemWithFont("Shader Debug", Menu::FontRole::Subheading)) { + if (MenuFonts::BeginTabItemWithFont(T("menu.advanced.tab_shader_debug", "Shader Debug"), Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##ShaderDebugContent", ImVec2(0, 0), false)) { RenderShaderDebugSection(); } @@ -59,7 +60,7 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } // Testing Tab (for A/B Testing and related settings) - if (MenuFonts::BeginTabItemWithFont("Testing", Menu::FontRole::Subheading)) { + if (MenuFonts::BeginTabItemWithFont(T("menu.advanced.tab_testing", "Testing"), Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##Testing", ImVec2(0, 0), false)) { RenderTestingSection(); } @@ -78,26 +79,26 @@ void AdvancedSettingsRenderer::RenderLoggingSection() // Log Level selection spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); const char* items[] = { - "trace", - "debug", - "info", - "warn", - "err", - "critical", - "off" + T("menu.advanced.log_level_trace", "trace"), + T("menu.advanced.log_level_debug", "debug"), + T("menu.advanced.log_level_info", "info"), + T("menu.advanced.log_level_warn", "warn"), + T("menu.advanced.log_level_err", "err"), + T("menu.advanced.log_level_critical", "critical"), + T("menu.advanced.log_level_off", "off") }; static int item_current = static_cast(logLevel); - if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + if (ImGui::Combo(T("menu.advanced.log_level", "Log Level"), &item_current, items, IM_ARRAYSIZE(items))) { ImGui::SameLine(); globals::state->SetLogLevel(static_cast(item_current)); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Log level. Trace is most verbose. Default is info."); + ImGui::Text("%s", T("menu.advanced.log_level_tooltip", "Log level. Trace is most verbose. Default is info.")); } // Shader Defines input auto& shaderDefines = globals::state->shaderDefinesString; - if (ImGui::InputText("Shader Defines", &shaderDefines)) { + if (ImGui::InputText(T("menu.advanced.shader_defines", "Shader Defines"), &shaderDefines)) { globals::state->SetDefines(shaderDefines); } if (ImGui::IsItemDeactivatedAfterEdit() || (ImGui::IsItemActive() && @@ -107,31 +108,31 @@ void AdvancedSettingsRenderer::RenderLoggingSection() shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); + ImGui::Text("%s", T("menu.advanced.shader_defines_tooltip", "Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile.")); } ImGui::Spacing(); // Compiler Thread controls - ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt(T("menu.advanced.compiler_threads", "Compiler Threads"), &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads used to compile shaders at startup. " - "Defaults to all logical cores minus one for OS headroom (E-cores included). " - "Higher values finish compilation faster but may make the system less responsive."); + ImGui::Text("%s", T("menu.advanced.compiler_threads_tooltip", + "Number of threads used to compile shaders at startup. " + "Defaults to all logical cores minus one for OS headroom (E-cores included). " + "Higher values finish compilation faster but may make the system less responsive.")); } - ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt(T("menu.advanced.background_compiler_threads", "Background Compiler Threads"), &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads used to compile shaders during gameplay. " - "Defaults to half of performance cores to avoid impacting the render thread. " - "Higher values finish compilation faster but may cause stuttering."); + ImGui::Text("%s", T("menu.advanced.background_compiler_threads_tooltip", + "Number of threads used to compile shaders during gameplay. " + "Defaults to half of performance cores to avoid impacting the render thread. " + "Higher values finish compilation faster but may cause stuttering.")); } ImGui::Columns(2, nullptr, false); // Dump Ini Settings button - if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { + if (ImGui::Button(T("menu.advanced.dump_ini_settings", "Dump Ini Settings"), { -1, 0 })) { Util::DumpSettingsOptions(); } @@ -139,7 +140,7 @@ void AdvancedSettingsRenderer::RenderLoggingSection() // Open Logs button std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); - if (!logPath.empty() && ImGui::Button("Open Logs", { -1, 0 })) { + if (!logPath.empty() && ImGui::Button(T("menu.advanced.open_logs", "Open Logs"), { -1, 0 })) { ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); } @@ -153,19 +154,19 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() // Dump Shaders option bool useDump = shaderCache->IsDump(); - if (ImGui::Checkbox("Dump Shaders", &useDump)) { + if (ImGui::Checkbox(T("menu.advanced.dump_shaders", "Dump Shaders"), &useDump)) { shaderCache->SetDump(useDump); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this."); + ImGui::Text("%s", T("menu.advanced.dump_shaders_tooltip", "Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this.")); } // Clear Shader Cache button - if (ImGui::Button("Clear Shader Cache", { -1, 0 })) { + if (ImGui::Button(T("menu.advanced.clear_shader_cache", "Clear Shader Cache"), { -1, 0 })) { shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Clear all compiled shaders from memory. Forces recompilation of all shaders on next use."); + ImGui::Text("%s", T("menu.advanced.clear_shader_cache_tooltip", "Clear all compiled shaders from memory. Forces recompilation of all shaders on next use.")); } ImGui::Spacing(); @@ -173,7 +174,7 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::Spacing(); // Shader Replacement section - Util::DrawSectionHeader("Replace Original Shaders"); + Util::DrawSectionHeader(T("menu.advanced.replace_original_shaders", "Replace Original Shaders")); if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { globals::state->ForEachShaderTypeWithIndex([&](auto type, int classIndex) { @@ -187,28 +188,28 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::Checkbox(std::format("{}", magic_enum::enum_name(type)).c_str(), &state->enabledClasses[classIndex]); }); if (state->IsDeveloperMode()) { - ImGui::Checkbox("Vertex", &state->enableVShaders); + ImGui::Checkbox(T("menu.advanced.vertex", "Vertex"), &state->enableVShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Vertex Shaders. " - "When false, will disable the custom Vertex Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("%s", T("menu.advanced.vertex_tooltip", + "Replace Vertex Shaders. " + "When false, will disable the custom Vertex Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. ")); } - ImGui::Checkbox("Pixel", &state->enablePShaders); + ImGui::Checkbox(T("menu.advanced.pixel", "Pixel"), &state->enablePShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Pixel Shaders. " - "When false, will disable the custom Pixel Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("%s", T("menu.advanced.pixel_tooltip", + "Replace Pixel Shaders. " + "When false, will disable the custom Pixel Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. ")); } - ImGui::Checkbox("Compute", &state->enableCShaders); + ImGui::Checkbox(T("menu.advanced.compute", "Compute"), &state->enableCShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Compute Shaders. " - "When false, will disable the custom Compute Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("%s", T("menu.advanced.compute_tooltip", + "Replace Compute Shaders. " + "When false, will disable the custom Compute Shaders for the types above. " + "For developers to test whether CS shaders match vanilla behavior. ")); } } ImGui::EndTable(); @@ -235,25 +236,26 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() float maxHeight = ImGui::GetContentRegionAvail().y * 0.3f; // Limit to 30% to keep Active Shaders visible if (ImGui::BeginChild("##BlockedShaderInfo", ImVec2(0, maxHeight), true, ImGuiChildFlags_AutoResizeY)) { - Util::Text::Error("Shader Blocking Active"); + Util::Text::Error(T("menu.advanced.shader_blocking_active", "Shader Blocking Active")); ImGui::SameLine(); - if (ImGui::SmallButton("Stop Blocking##Section")) { + if (ImGui::SmallButton(T("menu.advanced.stop_blocking", "Stop Blocking##Section"))) { shaderCache->DisableShaderBlocking(); } - ImGui::Text("Blocked: %s", shaderCache->blockedKey.c_str()); + ImGui::Text(T("menu.advanced.blocked_shader", "Blocked: %s"), shaderCache->blockedKey.c_str()); // Try to get more details from active shaders auto activeShaders = shaderCache->GetActiveShaders(); for (const auto& shader : activeShaders) { if (shader.key == shaderCache->blockedKey) { - ImGui::Text("Type: %s", magic_enum::enum_name(shader.shaderType).data()); - ImGui::Text("Class: %s", magic_enum::enum_name(shader.shaderClass).data()); - ImGui::Text("Descriptor: 0x%X", shader.descriptor); + ImGui::Text(T("menu.advanced.shader_type_label", "Type: %s"), magic_enum::enum_name(shader.shaderType).data()); + ImGui::Text(T("menu.advanced.shader_class_label", "Class: %s"), magic_enum::enum_name(shader.shaderClass).data()); + ImGui::Text(T("menu.advanced.shader_descriptor", "Descriptor: 0x%X"), shader.descriptor); // Add button to copy shader info to clipboard ImGui::PushID(shader.key.c_str()); - if (ImGui::SmallButton("Copy Info##BlockedShader")) { + auto copyInfoLabel = std::format("{}##BlockedShader", T("menu.advanced.copy_info", "Copy Info")); + if (ImGui::SmallButton(copyInfoLabel.c_str())) { std::string diskPathStr; diskPathStr.reserve(shader.diskPath.size()); for (wchar_t wc : shader.diskPath) { @@ -271,7 +273,7 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::PopID(); if (ImGui::IsItemHovered()) { if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Copy complete shader information including cache path to clipboard"); + ImGui::Text("%s", T("menu.advanced.copy_info_tooltip", "Copy complete shader information including cache path to clipboard")); } } @@ -287,16 +289,16 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } // Shader Debug section - if (ImGui::CollapsingHeader("Shader Debug")) { + if (ImGui::CollapsingHeader(T("menu.advanced.shader_debug_header", "Shader Debug"))) { auto menu = globals::menu; auto& menuSettings = menu->GetSettings(); auto& themeSettings = menuSettings.Theme; - if (ImGui::Checkbox("Enable Shader Blocking", &menuSettings.EnableShaderBlocking)) { + if (ImGui::Checkbox(T("menu.advanced.enable_shader_blocking", "Enable Shader Blocking"), &menuSettings.EnableShaderBlocking)) { // Setting saved automatically on next save } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enables hotkeys to cycle through and block individual shaders for debugging purposes."); + ImGui::Text("%s", T("menu.advanced.enable_shader_blocking_tooltip", "Enables hotkeys to cycle through and block individual shaders for debugging purposes.")); } if (menuSettings.EnableShaderBlocking) { @@ -304,32 +306,32 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() // Shader Block Previous Key if (menu->settingShaderBlockPrevKey) { - ImGui::Text("Press any key for Shader Block Previous..."); + ImGui::Text("%s", T("menu.advanced.press_key_shader_block_prev", "Press any key for Shader Block Previous...")); } else { ImGui::AlignTextToFramePadding(); - ImGui::Text("Block Previous:"); + ImGui::Text("%s", T("menu.advanced.block_previous", "Block Previous:")); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.ShaderBlockPrevKey).c_str()); ImGui::SameLine(); - if (ImGui::Button("Change##ShaderBlockPrev")) { + if (ImGui::Button(T("menu.advanced.change_shader_block_prev", "Change##ShaderBlockPrev"))) { menu->settingShaderBlockPrevKey = true; } } // Shader Block Next Key if (menu->settingShaderBlockNextKey) { - ImGui::Text("Press any key for Shader Block Next..."); + ImGui::Text("%s", T("menu.advanced.press_key_shader_block_next", "Press any key for Shader Block Next...")); } else { ImGui::AlignTextToFramePadding(); - ImGui::Text("Block Next:"); + ImGui::Text("%s", T("menu.advanced.block_next", "Block Next:")); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.ShaderBlockNextKey).c_str()); ImGui::SameLine(); - if (ImGui::Button("Change##ShaderBlockNext")) { + if (ImGui::Button(T("menu.advanced.change_shader_block_next", "Change##ShaderBlockNext"))) { menu->settingShaderBlockNextKey = true; } } @@ -339,13 +341,13 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } // Active shaders list - if (ImGui::CollapsingHeader("Active Shaders", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Active Shaders (Used Recently)"); + if (ImGui::CollapsingHeader(T("menu.advanced.active_shaders", "Active Shaders"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", T("menu.advanced.active_shaders_used_recently", "Active Shaders (Used Recently)")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "List of shaders that have been used in recent frames. " - "Enable Shader Blocking above to use hotkeys to cycle through and block shaders for debugging. " - "Shaders not used for ~1 second are removed from this list."); + ImGui::Text("%s", T("menu.advanced.active_shaders_tooltip", + "List of shaders that have been used in recent frames. " + "Enable Shader Blocking above to use hotkeys to cycle through and block shaders for debugging. " + "Shaders not used for ~1 second are removed from this list.")); } // Get fresh active shaders data for accurate count and table @@ -373,20 +375,20 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() // Build column configurations std::vector> columns = { - { "Type", "Shader type", [](const ShaderRow& row) { + { T("menu.advanced.column_type", "Type"), T("menu.advanced.column_type_tooltip", "Shader type"), [](const ShaderRow& row) { return std::string(magic_enum::enum_name(row.shader.shaderType)); } }, - { "Class", "Shader class", [](const ShaderRow& row) { + { T("menu.advanced.column_class", "Class"), T("menu.advanced.column_class_tooltip", "Shader class"), [](const ShaderRow& row) { return std::string(magic_enum::enum_name(row.shader.shaderClass)); } }, - { "Descriptor", "Shader descriptor", [](const ShaderRow& row) { + { T("menu.advanced.column_descriptor", "Descriptor"), T("menu.advanced.column_descriptor_tooltip", "Shader descriptor"), [](const ShaderRow& row) { return std::format("0x{:X}", row.shader.descriptor); } }, - { "Frame %", "Percentage of draw calls this frame", [](const ShaderRow& row) { + { T("menu.advanced.column_frame_pct", "Frame %"), T("menu.advanced.column_frame_pct_tooltip", "Percentage of draw calls this frame"), [](const ShaderRow& row) { float percentage = Util::CalculatePercentage(static_cast(row.shader.drawCalls), static_cast(row.totalDrawCalls)); return Util::FormatPercent(percentage); } }, - { "Key", "Shader key", [](const ShaderRow& row) { + { T("menu.advanced.column_key", "Key"), T("menu.advanced.column_key_tooltip", "Shader key"), [](const ShaderRow& row) { return row.shader.key; } } }; @@ -419,14 +421,16 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::SetClipboardText(fullInfo.c_str()); }; auto getRowTooltip = [shaderCache](const ShaderRow& row) { - std::string clickAction = (row.shader.key == shaderCache->blockedKey) ? "Left-click to unblock this shader" : "Left-click to block this shader"; - - return std::format("Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\n\n{}", - magic_enum::enum_name(row.shader.shaderType).data(), - magic_enum::enum_name(row.shader.shaderClass).data(), - row.shader.descriptor, - row.shader.key, - clickAction); + std::string clickAction = (row.shader.key == shaderCache->blockedKey) ? T("menu.advanced.click_to_unblock", "Left-click to unblock this shader") : T("menu.advanced.click_to_block", "Left-click to block this shader"); + auto shaderType = magic_enum::enum_name(row.shader.shaderType); + auto shaderClass = magic_enum::enum_name(row.shader.shaderClass); + + return std::vformat(T("menu.advanced.shader_row_tooltip", "Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\n\n{}"), std::make_format_args( + shaderType, + shaderClass, + row.shader.descriptor, + row.shader.key, + clickAction)); }; // Define function to extract filterable fields (for TableFilterState) @@ -482,7 +486,7 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() // Left-click to block/unblock shader { Util::TableInputEventType::MouseClick, onRowLeftClick, "", 0 }, // Right-click context menu for copying info - { Util::TableInputEventType::ContextMenu, onRowRightClick, "Copy Info", 1 } + { Util::TableInputEventType::ContextMenu, onRowRightClick, T("menu.advanced.copy_info", "Copy Info"), 1 } }; // Render the table with all configurations @@ -515,17 +519,17 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() // File Watcher option (moved from Advanced/Logging) bool useFileWatcher = shaderCache->UseFileWatcher(); - if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { + if (ImGui::Checkbox(T("menu.advanced.enable_file_watcher", "Enable File Watcher"), &useFileWatcher)) { shaderCache->SetFileWatcher(useFileWatcher); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Automatically recompile shaders on file change. " - "Intended for developing."); + ImGui::Text("%s", T("menu.advanced.enable_file_watcher_tooltip", + "Automatically recompile shaders on file change. " + "Intended for developing.")); } // Debug addresses section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Addresses")) { + if (ImGui::TreeNodeEx(T("menu.advanced.addresses", "Addresses"))) { auto Renderer = globals::game::renderer; auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); auto RendererShadowState = globals::game::shadowState; @@ -536,8 +540,9 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() } // Statistics section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); + if (ImGui::TreeNodeEx(T("menu.advanced.statistics", "Statistics"), ImGuiTreeNodeFlags_DefaultOpen)) { + std::string shaderStatsString = shaderCache->GetShaderStatsString(); + ImGui::Text("%s", std::vformat(T("menu.advanced.shader_compiler_stats", "Shader Compiler : {}"), std::make_format_args(shaderStatsString)).c_str()); // Derived parallelism metrics are computed lazily on demand and only shown // once compilation has completed to avoid per-frame analysis while compiling. @@ -546,66 +551,71 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() if (parallelism.has_value()) { const auto& p = parallelism.value(); ImGui::Spacing(); - ImGui::TextDisabled("Parallelism (derived from %zu compiled tasks)", p.sampleCount); + ImGui::TextDisabled(T("menu.advanced.parallelism_header", "Parallelism (derived from %zu compiled tasks)"), p.sampleCount); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Computed lazily from the last completed build."); - ImGui::Text("Only evaluated when this Statistics section is open."); + ImGui::Text("%s", T("menu.advanced.parallelism_tooltip_1", "Computed lazily from the last completed build.")); + ImGui::Text("%s", T("menu.advanced.parallelism_tooltip_2", "Only evaluated when this Statistics section is open.")); } - ImGui::Text("Work (W, sum of task wall times): %s", Util::FormatDuration(p.workMs).c_str()); + ImGui::Text(T("menu.advanced.work_metric", "Work (W, sum of task wall times): %s"), Util::FormatDuration(p.workMs).c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Total compile work: sum of all per-shader wall-clock compile times."); - ImGui::Text("This is not CPU time; it is accumulated task elapsed time."); - ImGui::Text("Equivalent serial time on one worker if overhead stayed the same."); + ImGui::Text("%s", T("menu.advanced.work_tooltip_1", "Total compile work: sum of all per-shader wall-clock compile times.")); + ImGui::Text("%s", T("menu.advanced.work_tooltip_2", "This is not CPU time; it is accumulated task elapsed time.")); + ImGui::Text("%s", T("menu.advanced.work_tooltip_3", "Equivalent serial time on one worker if overhead stayed the same.")); } - ImGui::Text("Span (S, longest): %s", Util::FormatDuration(p.spanMs).c_str()); + ImGui::Text(T("menu.advanced.span_metric", "Span (S, longest): %s"), Util::FormatDuration(p.spanMs).c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Critical-path lower bound, approximated by the single slowest shader."); - ImGui::Text("Even infinite cores cannot finish faster than this."); + ImGui::Text("%s", T("menu.advanced.span_tooltip_1", "Critical-path lower bound, approximated by the single slowest shader.")); + ImGui::Text("%s", T("menu.advanced.span_tooltip_2", "Even infinite cores cannot finish faster than this.")); } - ImGui::Text("Makespan (T_p): %s", Util::FormatDuration(p.makespanMs).c_str()); + ImGui::Text(T("menu.advanced.makespan_metric", "Makespan (T_p): %s"), Util::FormatDuration(p.makespanMs).c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Observed wall-clock duration for the full shader build."); + ImGui::Text("%s", T("menu.advanced.makespan_tooltip", "Observed wall-clock duration for the full shader build.")); } - ImGui::Text("Queue wait (avg/max): %s / %s", + ImGui::Text(T("menu.advanced.queue_wait_metric", "Queue wait (avg/max): %s / %s"), Util::FormatDuration(p.avgQueueWaitMs).c_str(), Util::FormatDuration(p.maxQueueWaitMs).c_str()); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time spent waiting in the ready queue before a worker started compilation."); - ImGui::Text("Useful for identifying scheduler-induced delay separate from compile cost."); + ImGui::Text("%s", T("menu.advanced.queue_wait_tooltip_1", "Time spent waiting in the ready queue before a worker started compilation.")); + ImGui::Text("%s", T("menu.advanced.queue_wait_tooltip_2", "Useful for identifying scheduler-induced delay separate from compile cost.")); } - ImGui::Text("Average parallelism (W/S): %.2fx", p.avgParallelism); + ImGui::Text(T("menu.advanced.avg_parallelism_metric", "Average parallelism (W/S): %.2fx"), p.avgParallelism); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Average useful concurrency in this workload."); - ImGui::Text("Roughly the worker count where adding more cores gives diminishing returns."); + ImGui::Text("%s", T("menu.advanced.avg_parallelism_tooltip_1", "Average useful concurrency in this workload.")); + ImGui::Text("%s", T("menu.advanced.avg_parallelism_tooltip_2", "Roughly the worker count where adding more cores gives diminishing returns.")); } - ImGui::Text("Infinite-core efficiency (S/T_p): %.1f%%", 100.0 * p.infiniteCoreEfficiency); + ImGui::Text(T("menu.advanced.infinite_core_efficiency_metric", "Infinite-core efficiency (S/T_p): %.1f%%"), 100.0 * p.infiniteCoreEfficiency); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How close runtime is to the infinite-core lower bound."); - ImGui::Text("100%% means T_p == S."); + ImGui::Text("%s", T("menu.advanced.infinite_core_efficiency_tooltip_1", "How close runtime is to the infinite-core lower bound.")); + ImGui::Text("%s", T("menu.advanced.infinite_core_efficiency_tooltip_2", "100%% means T_p == S.")); } - ImGui::Text("Infinite-core gap: %.1f%%", p.infiniteCoreGapPercent); + ImGui::Text(T("menu.advanced.infinite_core_gap_metric", "Infinite-core gap: %.1f%%"), p.infiniteCoreGapPercent); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Distance from ideal infinite-core time."); - ImGui::Text("Defined as 100 * (1 - S / T_p). Lower is better."); + ImGui::Text("%s", T("menu.advanced.infinite_core_gap_tooltip_1", "Distance from ideal infinite-core time.")); + ImGui::Text("%s", T("menu.advanced.infinite_core_gap_tooltip_2", "Defined as 100 * (1 - S / T_p). Lower is better.")); } ImGui::Spacing(); - ImGui::TextDisabled("Infinite-core efficiency"); + ImGui::TextDisabled("%s", T("menu.advanced.infinite_core_efficiency", "Infinite-core efficiency")); float efficiency = static_cast(std::clamp(p.infiniteCoreEfficiency, 0.0, 1.0)); - ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), std::format("{:.1f}% efficient / {:.1f}% gap", 100.0 * p.infiniteCoreEfficiency, p.infiniteCoreGapPercent).c_str()); + double efficiencyPercent = 100.0 * p.infiniteCoreEfficiency; + std::string efficiencyProgress = std::vformat(T("menu.advanced.efficiency_progress", "{:.1f}% efficient / {:.1f}% gap"), std::make_format_args(efficiencyPercent, p.infiniteCoreGapPercent)); + ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), efficiencyProgress.c_str()); ImGui::Spacing(); - ImGui::TextDisabled("Relative durations (normalized)"); + ImGui::TextDisabled("%s", T("menu.advanced.relative_durations", "Relative durations (normalized)")); double maxMs = std::max({ p.workMs, p.spanMs, p.makespanMs, 1.0 }); auto drawRelativeBar = [maxMs](const char* label, double value) { float ratio = static_cast(std::clamp(value / maxMs, 0.0, 1.0)); ImGui::TextUnformatted(label); ImGui::SameLine(); - ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), std::format("{} ({:.1f}%)", Util::FormatDuration(value), 100.0 * ratio).c_str()); + std::string duration = Util::FormatDuration(value); + double percent = 100.0 * ratio; + std::string progressText = std::vformat(T("menu.advanced.relative_bar_format", "{} ({:.1f}%)"), std::make_format_args(duration, percent)); + ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), progressText.c_str()); }; - drawRelativeBar("Span (S)", p.spanMs); - drawRelativeBar("Makespan (T_p)", p.makespanMs); - drawRelativeBar("Work (W)", p.workMs); + drawRelativeBar(T("menu.advanced.span_label", "Span (S)"), p.spanMs); + drawRelativeBar(T("menu.advanced.makespan_label", "Makespan (T_p)"), p.makespanMs); + drawRelativeBar(T("menu.advanced.work_label", "Work (W)"), p.workMs); } } @@ -613,10 +623,10 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() auto topSlow = shaderCache->GetTopSlowTasks(3); if (!topSlow.empty()) { ImGui::Spacing(); - ImGui::TextDisabled("Top %zu Slowest Shaders (last build)", topSlow.size()); + ImGui::TextDisabled(T("menu.advanced.top_slowest_shaders", "Top %zu Slowest Shaders (last build)"), topSlow.size()); for (size_t i = 0; i < topSlow.size(); ++i) { const auto& rec = topSlow[i]; - ImGui::Text("#%zu %s (weight %d)", i + 1, + ImGui::Text(T("menu.advanced.shader_slow_entry", "#%zu %s (weight %d)"), i + 1, Util::FormatDuration(rec.elapsedMs).c_str(), rec.priority); ImGui::SameLine(); ImGui::TextDisabled("%s", rec.key.c_str()); @@ -627,7 +637,7 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() } // Allow copying the full key with a right-click if (ImGui::BeginPopupContextItem(std::format("##slowcopy{}", i).c_str())) { - if (ImGui::MenuItem("Copy key")) { + if (ImGui::MenuItem(T("menu.advanced.copy_key", "Copy key"))) { ImGui::SetClipboardText(rec.key.c_str()); } ImGui::EndPopup(); @@ -639,46 +649,46 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() } // Frame annotations toggle (moved from Advanced/Logging) - ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); + ImGui::Checkbox(T("menu.advanced.frame_annotations", "Frame Annotations"), &globals::state->frameAnnotations); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); + ImGui::Text("%s", T("menu.advanced.frame_annotations_tooltip", "Enable detailed frame annotations for debugging render passes and draw calls.")); } // Half-precision (partial precision) shader compile flag bool partialPrecision = globals::state->enablePartialPrecision.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { + if (ImGui::Checkbox(T("menu.advanced.half_precision", "Half Precision (Partial Precision)"), &partialPrecision)) { globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); // Force a recompile so the flag actually takes effect on subsequent shader builds. globals::shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" - "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " - "on top of the existing min16float type hints.\n" - "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " - "pressure and double ALU throughput, but it can also introduce minor visual " - "differences in shaders that haven't been audited for precision sensitivity.\n" - "Toggling this clears the shader cache and triggers a full recompile."); + ImGui::Text("%s", T("menu.advanced.half_precision_tooltip", + "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" + "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " + "on top of the existing min16float type hints.\n" + "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " + "pressure and double ALU throughput, but it can also introduce minor visual " + "differences in shaders that haven't been audited for precision sensitivity.\n" + "Toggling this clears the shader cache and triggers a full recompile.")); } // Avoid flow control compiler flag (transient — not saved to config because the // right setting depends on the current scene, not the user). bool avoidFlowControl = globals::state->enableAvoidFlowControl.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { + if (ImGui::Checkbox(T("menu.advanced.avoid_flow_control", "Avoid Flow Control"), &avoidFlowControl)) { globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); // Force a recompile so the flag actually takes effect on subsequent shader builds. globals::shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" - "Forces fxc to flatten branches into predicated ops rather than emitting " - "dynamic flow control. Often a win for short branch bodies and uniformly-" - "taken branches; usually a loss for long divergent branches that vanilla " - "flow control would skip entirely.\n" - "Resets every launch. Toggling this clears the shader cache and triggers a " - "full recompile."); + ImGui::Text("%s", T("menu.advanced.avoid_flow_control_tooltip", + "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" + "Forces fxc to flatten branches into predicated ops rather than emitting " + "dynamic flow control. Often a win for short branch bodies and uniformly-" + "taken branches; usually a loss for long divergent branches that vanilla " + "flow control would skip entirely.\n" + "Resets every launch. Toggling this clears the shader cache and triggers a " + "full recompile.")); } ImGui::Spacing(); @@ -691,7 +701,7 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() ImGui::Spacing(); // Test Conditions button - runs a set of console commands to prepare the player for testing - if (ImGui::Button("Test Conditions", { -1, 0 })) { + if (ImGui::Button(T("menu.advanced.test_conditions", "Test Conditions"), { -1, 0 })) { if (auto ui = RE::UI::GetSingleton(); ui && !ui->menuStack.empty() && RE::PlayerCharacter::GetSingleton()) { RE::Console::ExecuteCommand("player.setav speedmult 1000"); RE::Console::ExecuteCommand("tgm"); diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 8764cc3af1..e52012bd3c 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -14,6 +14,7 @@ #include "FeatureIssues.h" #include "Fonts.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Menu/HomePageRenderer.h" #include "Menu/ProfilingRenderer.h" @@ -27,7 +28,25 @@ namespace { // Core built-in menu names that always appear first in the menu list - constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "Profiling", "Display" }; + // These are canonical identifiers used for logic — NOT translated + constexpr std::array CORE_MENU_NAMES = { + "Home", "General", "Advanced", "Profiling", "Display" + }; + + const char* GetCoreMenuDisplayName(const char* canonicalName) + { + if (std::strcmp(canonicalName, "Home") == 0) + return T("menu.features.home", "Home"); + if (std::strcmp(canonicalName, "General") == 0) + return T("menu.features.general", "General"); + if (std::strcmp(canonicalName, "Advanced") == 0) + return T("menu.features.advanced", "Advanced"); + if (std::strcmp(canonicalName, "Profiling") == 0) + return T("menu.features.profiling", "Profiling"); + if (std::strcmp(canonicalName, "Display") == 0) + return T("menu.features.display", "Display"); + return canonicalName; + } bool IsCoreMenu(const std::string& menuName) { @@ -110,6 +129,34 @@ namespace return MenuFonts::BeginTabItemWithFont(label, role, flags); } + std::string TranslateFeatureCategory(std::string_view category) + { + if (category == FeatureCategories::kCharacters) + return T("feature.category.characters", "Characters"); + if (category == FeatureCategories::kDisplay) + return T("feature.category.display", "Display"); + if (category == FeatureCategories::kGrass) + return T("feature.category.grass", "Grass"); + if (category == FeatureCategories::kLandscapeAndTextures) + return T("feature.category.landscape_and_textures", "Landscape & Textures"); + if (category == FeatureCategories::kLighting) + return T("feature.category.lighting", "Lighting"); + if (category == FeatureCategories::kMaterials) + return T("feature.category.materials", "Materials"); + if (category == "Post-Processing") + return T("feature.category.post_processing", "Post-Processing"); + if (category == FeatureCategories::kOther) + return T("feature.category.other", "Other"); + if (category == FeatureCategories::kSky) + return T("feature.category.sky", "Sky"); + if (category == FeatureCategories::kUtility) + return T("feature.category.utility", "Utility"); + if (category == FeatureCategories::kWater) + return T("feature.category.water", "Water"); + + return std::string(category); + } + /** * @brief Draws a feature header with the feature name in large text and version in smaller text * @param featureName The display name of the feature @@ -268,7 +315,7 @@ std::vector FeatureListRenderer::BuildMenuLis auto& featureList = Feature::GetFeatureList(); auto sortedFeatureList{ featureList }; // need a copy so the load order is not lost std::ranges::sort(sortedFeatureList, [](Feature* a, Feature* b) { - return a->GetName() < b->GetName(); + return a->GetDisplayName() < b->GetDisplayName(); }); // Filter features by search string @@ -279,10 +326,10 @@ std::vector FeatureListRenderer::BuildMenuLis } auto menuList = std::vector{ - BuiltInMenu{ "Home", []() { HomePageRenderer::RenderHomePage(); } }, - BuiltInMenu{ "General", drawGeneralSettings }, - BuiltInMenu{ "Advanced", drawAdvancedSettings }, - BuiltInMenu{ "Profiling", []() { ProfilingRenderer::RenderStatistics(); } } + BuiltInMenu{ T("menu.features.home", "Home"), []() { HomePageRenderer::RenderHomePage(); } }, + BuiltInMenu{ T("menu.features.general", "General"), drawGeneralSettings }, + BuiltInMenu{ T("menu.features.advanced", "Advanced"), drawAdvancedSettings }, + BuiltInMenu{ T("menu.features.profiling", "Profiling"), []() { ProfilingRenderer::RenderStatistics(); } } }; // NOTE: The menu list is rebuilt every frame, so category expansion states // persist correctly. This is acceptable since the list is small and built // infrequently, but could be optimized if performance becomes an issue. @@ -299,7 +346,7 @@ std::vector FeatureListRenderer::BuildMenuLis // Sort features within each category for (auto& [category, features] : categorizedFeatures) { std::ranges::sort(features, [](Feature* a, Feature* b) { - return a->GetName() < b->GetName(); + return a->GetDisplayName() < b->GetDisplayName(); }); } @@ -345,12 +392,12 @@ std::vector FeatureListRenderer::BuildMenuLis return !feat->loaded && feat->IsInMenu() && (!FeatureIssues::IsObsoleteFeature(feat->GetShortName()) || globals::state->IsDeveloperMode()); }); if (std::ranges::distance(unloadedFeatures) != 0) { - menuList.push_back("Unloaded Features"s); + menuList.push_back(T("menu.features.unloaded_features", "Unloaded Features")); std::ranges::copy(unloadedFeatures, std::back_inserter(menuList)); } // Add top section for feature issues (rejected features, obsolete info, etc.) if (FeatureIssues::HasFeatureIssues()) { - menuList.insert(menuList.begin(), BuiltInMenu{ "Feature Issues", []() { + menuList.insert(menuList.begin(), BuiltInMenu{ T("menu.features.feature_issues", "Feature Issues"), []() { FeatureIssues::DrawFeatureIssuesUI(); } }); } @@ -413,7 +460,7 @@ void FeatureListRenderer::RenderLeftColumn( } // Add Features header and search bar after built-in settings - Util::DrawSectionHeader("Features", true); + Util::DrawSectionHeader(T("menu.features.features", "Features"), true); Util::DrawFeatureSearchBar(featureSearch); // Then render the rest (features and categories, but skip already rendered core menus) @@ -443,7 +490,7 @@ void FeatureListRenderer::RenderRightColumn( if (selectedMenu < menuList.size()) { std::visit(DrawMenuVisitor{ pendingFeatureSelection }, menuList[selectedMenu]); } else { - ImGui::TextDisabled("Please select an item on the left."); + ImGui::TextDisabled("%s", T("menu.features.select_item_left", "Please select an item on the left.")); } } @@ -452,7 +499,7 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const BuiltInMenu& menu) MenuFonts::FontRoleGuard fontGuard(Menu::FontRole::Subheading); // Use error color for Feature Issues menu item - bool isFeatureIssues = (menu.name == "Feature Issues"); + bool isFeatureIssues = (menu.name == T("menu.features.feature_issues", "Feature Issues")); if (isFeatureIssues) { auto& themeSettings = globals::menu->GetSettings().Theme; ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.Error); @@ -470,7 +517,7 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const BuiltInMenu& menu) void FeatureListRenderer::ListMenuVisitor::operator()(const std::string& label) { // Style "Unloaded Features" to match category headers - if (label == "Unloaded Features") { + if (label == T("menu.features.unloaded_features", "Unloaded Features")) { Util::DrawSectionHeader(label.c_str(), true); } else { // Use default separator text for other labels - should be themed via ImGuiCol_Separator @@ -488,7 +535,8 @@ void FeatureListRenderer::ListMenuVisitor::operator()(const CategoryHeader& head { MenuFonts::FontRoleGuard fontGuard(Menu::FontRole::Heading); int count = Menu::categoryCounts[std::string(header.name)]; - Util::DrawCategoryHeader(header.name.c_str(), isExpanded, count); + const auto categoryLabel = TranslateFeatureCategory(header.name); + Util::DrawCategoryHeader(header.name.c_str(), categoryLabel.c_str(), isExpanded, count); } // Update expansion state @@ -527,7 +575,7 @@ void FeatureListRenderer::ListMenuVisitor::operator()(Feature* feat) // Create selectable item with semantic color ImGui::PushStyleColor(ImGuiCol_Text, textColor); - if (ImGui::Selectable(fmt::format(" {} ", feat->GetName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { + if (ImGui::Selectable(fmt::format(" {} ", feat->GetDisplayName()).c_str(), selectedMenuRef == listId, ImGuiSelectableFlags_SpanAllColumns)) { selectedMenuRef = listId; } ImGui::PopStyleColor(); @@ -545,7 +593,7 @@ void FeatureListRenderer::DrawMenuVisitor::operator()(const BuiltInMenu& menu) { if (ImGui::BeginChild("##FeatureConfigFrame", { 0, 0 }, true)) { // Add spacing only for Home menu - if (menu.name == "Home") { + if (menu.name == T("menu.features.home", "Home")) { ImGui::Dummy(ImVec2(0, ThemeManager::Constants::BUTTON_SPACING)); } menu.func(); @@ -562,7 +610,7 @@ void FeatureListRenderer::DrawMenuVisitor::operator()(const std::string&) void FeatureListRenderer::DrawMenuVisitor::operator()(const CategoryHeader&) { // Category headers are not selectable in the right panel - ImGui::TextDisabled("Please select a feature from the left."); + ImGui::TextDisabled("%s", T("menu.features.select_feature_left", "Please select a feature from the left.")); } void FeatureListRenderer::DrawMenuVisitor::operator()(Feature* feat) @@ -607,7 +655,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureHeader(Feature* feat, bo float buttonPadding = ThemeManager::Constants::BUTTON_PADDING; float buttonSpacing = ThemeManager::Constants::BUTTON_SPACING; - const char* overrideButtonText = "Apply Override"; + const char* overrideButtonText = T("menu.features.apply_override", "Apply Override"); float bootToggleWidth = ImGui::GetFrameHeight() * 1.6f; float overrideButtonWidth = ImGui::CalcTextSize(overrideButtonText).x + buttonPadding; @@ -632,7 +680,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureHeader(Feature* feat, bo // Draw feature title, version, and description on the left // Returns title-only height for button alignment - float titleOnlyHeight = DrawFeatureHeader(feat->GetName(), isLoaded ? feat->version : "", description); + float titleOnlyHeight = DrawFeatureHeader(feat->GetDisplayName(), isLoaded ? feat->version : "", description); // Save cursor position after header (for restoring after buttons are drawn) ImVec2 cursorPosAfterHeader = ImGui::GetCursorScreenPos(); @@ -664,11 +712,12 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureHeader(Feature* feat, bo if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Toggle feature loading at boot.\n" - "Current state: %s\n" - "Restart required for changes to take effect.\n" - "Disabling removes performance impact.", - bootEnabled ? "Enabled" : "Disabled"); + T("menu.features.boot_toggle_tooltip", + "Toggle feature loading at boot.\n" + "Current state: %s\n" + "Restart required for changes to take effect.\n" + "Disabling removes performance impact."), + bootEnabled ? T("menu.features.enabled", "Enabled") : T("menu.features.disabled", "Disabled")); } // Apply Override button (when feature has available overrides) @@ -689,13 +738,17 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureHeader(Feature* feat, bo if (auto _tt = Util::HoverTooltipWrapper()) { if (sceneControlled) { ImGui::Text( - "Cannot apply overrides while scene-specific settings are active.\n" - "Pause scene settings for this feature first."); + "%s", + T("menu.features.cannot_apply_overrides_scene", + "Cannot apply overrides while scene-specific settings are active.\n" + "Pause scene settings for this feature first.")); } else { ImGui::Text( - "Restores original override settings from mod files.\n" - "This will discard your customizations and revert to\n" - "the mod author's recommended settings."); + "%s", + T("menu.features.restore_override_tooltip", + "Restores original override settings from mod files.\n" + "This will discard your customizations and revert to\n" + "the mod author's recommended settings.")); } } } @@ -709,21 +762,23 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, auto& themeSettings = globals::menu->GetSettings().Theme; if (isDisabled) { - ImGui::TextColored(themeSettings.StatusPalette.Disable, "Feature settings are hidden because this feature is disabled at boot."); + ImGui::TextColored(themeSettings.StatusPalette.Disable, "%s", T("menu.features.settings_hidden_disabled", "Feature settings are hidden because this feature is disabled at boot.")); ImGui::Spacing(); - ImGui::Text("Enable the feature above to access its configuration options."); + ImGui::Text("%s", T("menu.features.enable_to_access_config", "Enable the feature above to access its configuration options.")); } else { if (isLoaded) { auto weatherRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); if (weatherRegistry->HasWeatherSupport(feat->GetShortName())) { bool paused = weatherRegistry->IsFeaturePaused(feat->GetShortName()); - if (ImGui::Checkbox("Pause Weather Overrides", &paused)) { + if (ImGui::Checkbox(T("menu.features.pause_weather_overrides", "Pause Weather Overrides"), &paused)) { weatherRegistry->SetFeaturePaused(feat->GetShortName(), paused); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( - "Temporarily disable weather-based setting adjustments for this feature.\n" - "This state is not saved."); + "%s", + T("menu.features.pause_weather_tooltip", + "Temporarily disable weather-based setting adjustments for this feature.\n" + "This state is not saved.")); } ImGui::Separator(); } @@ -739,9 +794,10 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, if (Util::FeatureToggle("##PauseSceneSettings", &active)) sceneMgr->SetFeaturePaused(featureShortName, !active); ImGui::SameLine(); - ImGui::Text("Scene Specific Settings"); + ImGui::Text("%s", T("menu.features.scene_specific_settings", "Scene Specific Settings")); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text(scenePaused ? "Paused - click to resume" : "Active - click to pause"); + ImGui::Text("%s", T(scenePaused ? "menu.features.scene_paused_tooltip" : "menu.features.scene_active_tooltip", + scenePaused ? "Paused - click to resume" : "Active - click to pause")); } ImGui::Separator(); } @@ -808,23 +864,25 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, bool cursorMoved = (std::abs(cursorPosAfter.x - cursorPosBefore.x) > cursorEpsilon || std::abs(cursorPosAfter.y - cursorPosBefore.y) > cursorEpsilon); if (!cursorMoved) { - ImGui::TextColored(themeSettings.StatusPalette.Disable, "There are no settings available for this feature."); + ImGui::TextColored(themeSettings.StatusPalette.Disable, "%s", T("menu.features.no_settings_available", "There are no settings available for this feature.")); } } else { if (FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { feat->DrawUnloadedUI(); } else if (IsFeatureInstalled(feat->GetShortName())) { - ImGui::Text("This feature will be available after restart."); + ImGui::Text("%s", T("menu.features.available_after_restart", "This feature will be available after restart.")); } else { feat->DrawUnloadedUI(); if (!feat->GetFeatureModLink().empty()) { ImGui::Spacing(); - const auto downloadText = fmt::format("Click here to download this feature ({})", feat->GetFeatureModLink()); + auto featureModLink = feat->GetFeatureModLink(); + const auto downloadText = std::vformat( + T("menu.features.download_link", "Click here to download this feature ({})"), std::make_format_args(featureModLink)); if (ImGui::Selectable(downloadText.c_str())) { - ShellExecuteA(NULL, "open", feat->GetFeatureModLink().c_str(), NULL, NULL, SW_SHOWNORMAL); + ShellExecuteA(NULL, "open", featureModLink.c_str(), NULL, NULL, SW_SHOWNORMAL); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Download the feature from the mod page."); + ImGui::Text("%s", T("menu.features.download_tooltip", "Download the feature from the mod page.")); } } } @@ -833,7 +891,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, if (hasFailedMessage && feat->DrawFailLoadMessage() && !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { ImGui::Spacing(); - SeparatorTextWithFont("Error", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.features.error_header", "Error"), Menu::FontRole::Subheading); ImGui::TextColored(themeSettings.StatusPalette.Error, feat->failedLoadedMessage.c_str()); } } @@ -874,7 +932,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderRestoreDefaultsButton(Feature* ImGui::PopStyleColor(3); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Restore default settings for this feature"); + ImGui::Text("%s", T("menu.features.restore_defaults_tooltip", "Restore default settings for this feature")); } } @@ -894,17 +952,17 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("Setting Change Warning", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::TextWrapped("Some of your settings have been automatically adjusted due to feature incompatibilities."); + ImGui::TextWrapped("%s", T("menu.features.settings_adjusted_warning", "Some of your settings have been automatically adjusted due to feature incompatibilities.")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Table columns: Impacted Feature | Setting | Constrained By | Forced To if (ImGui::BeginTable("##ReactiveConstraintTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Impacted Feature", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Setting", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Constrained By", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Forced To", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T("menu.features.col_impacted_feature", "Impacted Feature"), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T("menu.features.col_setting", "Setting"), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T("menu.features.col_constrained_by", "Constrained By"), ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn(T("menu.features.col_forced_to", "Forced To"), ImGuiTableColumnFlags_WidthStretch); ImGui::TableHeadersRow(); size_t rowIndex = 0; @@ -918,7 +976,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog std::string targetDisplayName = settingId.featureShortName; for (auto* f : Feature::GetFeatureList()) { if (f->GetShortName() == settingId.featureShortName) { - targetDisplayName = f->GetName(); + targetDisplayName = f->GetDisplayName(); break; } } @@ -930,7 +988,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog return; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to navigate to %s", targetDisplayName.c_str()); + ImGui::Text(T("menu.features.click_to_navigate", "Click to navigate to %s"), targetDisplayName.c_str()); } } @@ -949,11 +1007,11 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog return; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to navigate to %s", result.sources[0].featureName.c_str()); + ImGui::Text(T("menu.features.click_to_navigate", "Click to navigate to %s"), result.sources[0].featureName.c_str()); if (result.sources.size() > 1) { ImGui::Separator(); for (size_t i = 1; i < result.sources.size(); ++i) { - ImGui::Text("Also: %s", result.sources[i].featureName.c_str()); + ImGui::Text(T("menu.features.also_feature", "Also: %s"), result.sources[i].featureName.c_str()); } } ImGui::Separator(); @@ -976,13 +1034,15 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog ImGui::Spacing(); ImGui::TextWrapped( - "These settings are disabled in their respective feature menus while the constraints are active. " - "Adjust the constraining features to remove them."); + "%s", + T("menu.features.constraints_explanation", + "These settings are disabled in their respective feature menus while the constraints are active. " + "Adjust the constraining features to remove them.")); ImGui::Spacing(); // "Don't show again" checkbox -- same pattern as Clear Cache dialog - ImGui::Checkbox("Don't show this warning again", &g_dontShowAgainCheckbox); + ImGui::Checkbox(T("menu.features.dont_show_warning", "Don't show this warning again"), &g_dontShowAgainCheckbox); ImGui::Spacing(); @@ -993,7 +1053,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog if (offset > 0) ImGui::SetCursorPosX(offset); - if (ImGui::Button("OK", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T("menu.features.ok_button", "OK"), ImVec2(buttonWidth, 0))) { if (g_dontShowAgainCheckbox) { if (auto* menu = globals::menu) { menu->GetSettings().SkipConstraintWarning = true; @@ -1010,4 +1070,4 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog g_reactiveWarningShow = false; g_reactiveWarningConstraints.clear(); } -} \ No newline at end of file +} diff --git a/src/Menu/Fonts.cpp b/src/Menu/Fonts.cpp index c11d20daa5..b31433d3ea 100644 --- a/src/Menu/Fonts.cpp +++ b/src/Menu/Fonts.cpp @@ -1,6 +1,7 @@ #include "Fonts.h" #include "../Globals.h" +#include "../I18n/I18n.h" #include "../Utils/FileSystem.h" #include "ThemeManager.h" @@ -195,6 +196,8 @@ namespace MenuFonts signature += std::format("{}|{}|{:.2f};", Menu::GetFontRoleKey(role), roleSettings.File, roundedSize); } signature += std::format("base|{:.2f};", std::round(baseFontSize)); + // Include locale in signature so CJK font merging triggers a rebuild on language change + signature += std::format("locale|{};", I18n::GetSingleton()->GetCurrentLocale()); return signature; } } // namespace MenuFonts diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index 7cf6a55ceb..5547f56756 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -5,6 +5,7 @@ #include "FeatureConstraints.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Plugin.h" #include "State.h" @@ -66,7 +67,8 @@ void HomePageRenderer::RenderWelcomeSection() ImVec2 windowSize = ImGui::GetWindowSize(); auto versionStr = Util::GetFormattedVersion(Plugin::VERSION); auto expectedTag = std::format("v{}", versionStr); - std::string titleWithVersion = Plugin::BUILD_DESCRIBE == expectedTag ? std::format("Welcome to Community Shaders {}", versionStr) : std::format("Welcome to Community Shaders {} [{}]", versionStr, Plugin::BUILD_DESCRIBE); + auto* i18n = I18n::GetSingleton(); + std::string titleWithVersion = Plugin::BUILD_DESCRIBE == expectedTag ? i18n->Format("menu.home.welcome", { { "version", versionStr } }, "Welcome to Community Shaders {version}") : i18n->Format("menu.home.welcome_dev", { { "version", versionStr }, { "build", std::string(Plugin::BUILD_DESCRIBE) } }, "Welcome to Community Shaders {version} [{build}]"); ImVec2 titleSize = ImGui::CalcTextSize(titleWithVersion.c_str()); ImGui::SetCursorPosX((windowSize.x - titleSize.x) * 0.5f); ImGui::Text("%s", titleWithVersion.c_str()); @@ -82,10 +84,10 @@ void HomePageRenderer::RenderWelcomeSection() ImGui::Spacing(); // Intro text - centered - const char* introText = + const char* introText = T("menu.home.intro", "Community Shaders provides advanced graphics enhancements for Skyrim.\n" "This comprehensive collection of features brings modern rendering techniques\n" - "to enhance your visual experience."; + "to enhance your visual experience."); ImVec2 introSize = ImGui::CalcTextSize(introText); ImGui::SetCursorPosX((windowSize.x - introSize.x) * 0.5f); ImGui::TextWrapped("%s", introText); @@ -131,15 +133,15 @@ void HomePageRenderer::RenderWelcomeSection() ImGui::PopStyleColor(3); ImGui::PopStyleVar(); - Util::AddTooltip("Join Community Shaders Discord Server"); + Util::AddTooltip(T("menu.home.join_discord", "Join our Discord")); } else { // Fallback button when Discord icon is not available float buttonWidth = DISCORD_BANNER_MIN_WIDTH * scale; ImGui::SetCursorPosX((windowSize.x - buttonWidth) * 0.5f); - if (ImGui::Button("Join Discord Server", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T("menu.home.join_discord", "Join our Discord"), ImVec2(buttonWidth, 0))) { ShellExecuteA(NULL, "open", DISCORD_URL, NULL, NULL, SW_SHOWNORMAL); } - Util::AddTooltip("Join Community Shaders Discord Server"); + Util::AddTooltip(T("menu.home.join_discord", "Join our Discord")); } ImGui::PopStyleVar(); @@ -149,29 +151,30 @@ void HomePageRenderer::RenderQuickLinksSection() { // Quick Links title - centered ImVec2 windowSize = ImGui::GetWindowSize(); - ImVec2 titleSize = ImGui::CalcTextSize("Quick Links"); + const char* quickLinksTitle = T("menu.home.quick_links", "Quick Links"); + ImVec2 titleSize = ImGui::CalcTextSize(quickLinksTitle); ImGui::SetCursorPosX((windowSize.x - titleSize.x) * 0.5f); - ImGui::Text("Quick Links"); + ImGui::Text("%s", quickLinksTitle); ImGui::Columns(4, nullptr, false); // External links in a row - if (ImGui::Button("Nexus Mods", ImVec2(-1, 0))) { + if (ImGui::Button(T("menu.home.nexus_mods", "Nexus Mods"), ImVec2(-1, 0))) { ShellExecuteA(NULL, "open", "https://www.nexusmods.com/skyrimspecialedition/mods/86492", NULL, NULL, SW_SHOWNORMAL); } ImGui::NextColumn(); - if (ImGui::Button("GitHub", ImVec2(-1, 0))) { + if (ImGui::Button(T("menu.home.github", "GitHub"), ImVec2(-1, 0))) { ShellExecuteA(NULL, "open", "https://github.com/doodlum/skyrim-community-shaders", NULL, NULL, SW_SHOWNORMAL); } ImGui::NextColumn(); - if (ImGui::Button("Wiki", ImVec2(-1, 0))) { + if (ImGui::Button(T("menu.home.wiki", "Wiki"), ImVec2(-1, 0))) { ShellExecuteA(NULL, "open", "https://modding.wiki/en/skyrim/developers/community-shaders", NULL, NULL, SW_SHOWNORMAL); } ImGui::NextColumn(); - if (ImGui::Button("Developer Wiki", ImVec2(-1, 0))) { + if (ImGui::Button(T("menu.home.dev_wiki", "Developer Wiki"), ImVec2(-1, 0))) { ShellExecuteA(NULL, "open", "https://github.com/doodlum/skyrim-community-shaders/wiki", NULL, NULL, SW_SHOWNORMAL); } @@ -182,75 +185,76 @@ void HomePageRenderer::RenderFAQSection() { // FAQ title - centered ImVec2 windowSize = ImGui::GetWindowSize(); - ImVec2 titleSize = ImGui::CalcTextSize("Frequently Asked Questions"); + const char* faqTitle = T("menu.faq.title", "Frequently Asked Questions"); + ImVec2 titleSize = ImGui::CalcTextSize(faqTitle); ImGui::SetCursorPosX((windowSize.x - titleSize.x) * 0.5f); - ImGui::Text("Frequently Asked Questions"); + ImGui::Text("%s", faqTitle); ImGui::Separator(); // FAQ items with collapsible headers - if (ImGui::CollapsingHeader("What is Community Shaders?")) { - ImGui::TextWrapped( - "Community Shaders is a comprehensive graphics enhancement framework for Skyrim that " - "provides advanced lighting, materials, and visual effects. It's designed to be modular, " - "allowing you to enable only the features you want while maintaining good performance."); + if (ImGui::CollapsingHeader(T("menu.faq.q1", "What is Community Shaders?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a1", + "Community Shaders is a comprehensive graphics enhancement framework for Skyrim that " + "provides advanced lighting, materials, and visual effects. It's designed to be modular, " + "allowing you to enable only the features you want while maintaining good performance.")); } - if (ImGui::CollapsingHeader("How do I configure features?")) { - ImGui::TextWrapped( - "Each feature can be found in the left sidebar menu. Click on any feature to access its " - "settings. Most features include presets and detailed tooltips to help you understand " - "what each setting does."); + if (ImGui::CollapsingHeader(T("menu.faq.q2", "How do I configure features?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a2", + "Each feature can be found in the left sidebar menu. Click on any feature to access its " + "settings. Most features include presets and detailed tooltips to help you understand " + "what each setting does.")); } - if (ImGui::CollapsingHeader("Why are some features not loading?")) { - ImGui::TextWrapped( - "Features may fail to load due to hardware incompatibility, missing dependencies, or " - "conflicts with other mods. Check the 'Feature Issues' tab for detailed information " - "about any problematic features."); + if (ImGui::CollapsingHeader(T("menu.faq.q3", "Why are some features not loading?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a3", + "Features may fail to load due to hardware incompatibility, missing dependencies, or " + "conflicts with other mods. Check the 'Feature Issues' tab for detailed information " + "about any problematic features.")); } - if (ImGui::CollapsingHeader("I have \"Failed Shaders\" when compiling?")) { - ImGui::TextWrapped( - "Failed shaders are usually caused by mixed file versions. Ensure all features are up to date " - "and avoid mixing files from test builds or outdated versions. Please review the 'Feature Issues' tab " - "and/or Wiki for more information. Update your features and remove any obsolete features."); + if (ImGui::CollapsingHeader(T("menu.faq.q4", "I have \"Failed Shaders\" when compiling?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a4", + "Failed shaders are usually caused by mixed file versions. Ensure all features are up to date " + "and avoid mixing files from test builds or outdated versions. Please review the 'Feature Issues' tab " + "and/or Wiki for more information. Update your features and remove any obsolete features.")); } - if (ImGui::CollapsingHeader("How do I improve performance?")) { - ImGui::TextWrapped( - "Start by enabling the Performance Overlay to monitor your FPS. Consider disabling " - "expensive features like Screen Space GI or reducing quality settings. The 'Display' " - "tab also includes upscaling options that can improve performance."); + if (ImGui::CollapsingHeader(T("menu.faq.q5", "How do I improve performance?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a5", + "Start by enabling the Performance Overlay to monitor your FPS. Consider disabling " + "expensive features like Screen Space GI or reducing quality settings. The 'Display' " + "tab also includes upscaling options that can improve performance.")); } - if (ImGui::CollapsingHeader("Is Community Shaders compatible with ENB?")) { - ImGui::TextWrapped( - "No, Community Shaders is not compatible with ENB. Community Shaders will automatically " - "disable itself if ENB is detected."); + if (ImGui::CollapsingHeader(T("menu.faq.q6", "Is Community Shaders compatible with ENB?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a6", + "No, Community Shaders is not compatible with ENB. Community Shaders will automatically " + "disable itself if ENB is detected.")); } - if (ImGui::CollapsingHeader("The menu hotkey isn't working!")) { - ImGui::TextWrapped( - "By default, Community Shaders uses the END key to open this menu. If your keyboard " - "doesn't have an END key or it's not working, you can change it in the General > Keybindings tab. " - "You can also edit the hotkey in the JSON configuration files."); + if (ImGui::CollapsingHeader(T("menu.faq.q7", "The menu hotkey isn't working!"))) { + ImGui::TextWrapped("%s", T("menu.faq.a7", + "By default, Community Shaders uses the END key to open this menu. If your keyboard " + "doesn't have an END key or it's not working, you can change it in the General > Keybindings tab. " + "You can also edit the hotkey in the JSON configuration files.")); } - if (ImGui::CollapsingHeader("I would like to help develop Community Shaders.")) { - ImGui::TextWrapped( - "We're always looking for talented developers to join the team! Check out our GitHub wiki " - "for contribution guidelines and join our Discord server to connect with the development team. " - "Whether you're interested in shader programming, C++ development, or documentation, there's " - "always something to contribute."); + if (ImGui::CollapsingHeader(T("menu.faq.q8", "I would like to help develop Community Shaders."))) { + ImGui::TextWrapped("%s", T("menu.faq.a8", + "We're always looking for talented developers to join the team! Check out our GitHub wiki " + "for contribution guidelines and join our Discord server to connect with the development team. " + "Whether you're interested in shader programming, C++ development, or documentation, there's " + "always something to contribute.")); } - if (ImGui::CollapsingHeader("Is Community Shaders open source?")) { - ImGui::TextWrapped( - "Yes! Community Shaders is completely open source and available on GitHub. You can view " - "the source code, report issues, suggest features, and contribute to the project. " - "The project is licensed under GPL, ensuring it remains free and open for everyone." - " Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence." - " Any included assets may not be used without explicit permission."); + if (ImGui::CollapsingHeader(T("menu.faq.q9", "Is Community Shaders open source?"))) { + ImGui::TextWrapped("%s", T("menu.faq.a9", + "Yes! Community Shaders is completely open source and available on GitHub. You can view " + "the source code, report issues, suggest features, and contribute to the project. " + "The project is licensed under GPL, ensuring it remains free and open for everyone." + " Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence." + " Any included assets may not be used without explicit permission.")); } } @@ -268,12 +272,11 @@ void HomePageRenderer::RenderActiveConstraintsSection() ImVec4 warningColor = menu ? menu->GetTheme().StatusPalette.Warning : ImVec4(1.0f, 0.8f, 0.2f, 1.0f); ImGui::PushStyleColor(ImGuiCol_Text, warningColor); - bool headerOpen = ImGui::CollapsingHeader("Active Setting Constraints", ImGuiTreeNodeFlags_None); + bool headerOpen = ImGui::CollapsingHeader(T("menu.home.active_constraints", "Active Setting Constraints"), ImGuiTreeNodeFlags_None); ImGui::PopStyleColor(); if (headerOpen) { - ImGui::TextWrapped( - "Some settings are constrained by other features. Hover over rows for details."); + ImGui::TextWrapped("%s", T("menu.home.constraints_desc", "Some settings are constrained by other features. Hover over rows for details.")); ImGui::Spacing(); @@ -306,14 +309,14 @@ void HomePageRenderer::RenderActiveConstraintsSection() row.tooltip += "\n"; row.tooltip += std::format("{}: {}", src.featureName, src.reason); if (src.recommendDisableAtBoot) { - row.tooltip += "\nConsider disabling at boot."; + row.tooltip += std::string("\n") + T("menu.home.consider_disabling_at_boot", "Consider disabling at boot."); } } rows.push_back(row); } // Define headers - std::vector headers = { "Setting", "Forced To", "Constrained By" }; + std::vector headers = { T("menu.home.constraint_header_setting", "Setting"), T("menu.home.constraint_header_forced_to", "Forced To"), T("menu.home.constraint_header_constrained_by", "Constrained By") }; // Custom sorts (string comparators for each column) std::vector> customSorts = { @@ -337,7 +340,10 @@ void HomePageRenderer::RenderActiveConstraintsSection() } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to navigate to %s", row.constrainedBy.c_str()); + ImGui::Text("%s", I18n::GetSingleton()->Format("menu.home.click_to_navigate", + { { "feature", row.constrainedBy } }, + "Click to navigate to {feature}") + .c_str()); if (!row.tooltip.empty()) { ImGui::Separator(); ImGui::Text("%s", row.tooltip.c_str()); @@ -443,8 +449,8 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() }; // Version text - two lines, both centered (reduced spacing between lines) - const char* versionLine1 = "This appears to be a new install, update, or"; - const char* versionLine2 = "reinstallation of Community Shaders."; + const char* versionLine1 = T("menu.setup.new_install_line1", "This appears to be a new install, update, or"); + const char* versionLine2 = T("menu.setup.new_install_line2", "reinstallation of Community Shaders."); centerText(versionLine1); ImGui::Text("%s", versionLine1); @@ -455,7 +461,7 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGui::Spacing(); // Description - centered - const char* description = "Please choose a hotkey to access the menu:"; + const char* description = T("menu.setup.choose_hotkey", "Please choose a hotkey to access the menu:"); centerText(description); ImGui::Text("%s", description); @@ -518,20 +524,22 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() // Show hotkey capture message when in capture mode if (isCapturing) { - const char* pressKeyText = "Press any key to set as toggle key..."; + const char* pressKeyText = T("menu.setup.press_any_key", "Press any key to set as toggle key..."); centerText(pressKeyText); ImGui::TextDisabled("%s", pressKeyText); } // CS Editor hotkey status — updates live as user picks keys { - auto& weatherKey = menu->GetSettings().CSEditorToggleKey; - if (weatherKey.empty()) { - const char* warnText = "CS Editor hotkey unbound \xe2\x80\x94 chosen key uses Shift"; + auto& csEditorKey = menu->GetSettings().CSEditorToggleKey; + if (csEditorKey.empty()) { + const char* warnText = T("menu.setup.cs_editor_unbound", "CS Editor hotkey unbound - chosen key uses Shift"); centerText(warnText); ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.0f, 1.0f), "%s", warnText); } else { - std::string infoStr = "CS Editor hotkey will be: " + Util::Input::KeyIdToString(weatherKey); + std::string infoStr = I18n::GetSingleton()->Format("menu.setup.cs_editor_will_be", + { { "key", Util::Input::KeyIdToString(csEditorKey) } }, + "CS Editor hotkey will be: {key}"); centerText(infoStr.c_str()); ImGui::TextDisabled("%s", infoStr.c_str()); } @@ -539,7 +547,7 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() ImGui::Spacing(); - const char* laterText = "You can change this later in General > Keybindings."; + const char* laterText = T("menu.setup.change_later", "You can change this later in General > Keybindings."); centerText(laterText); ImGui::Text("%s", laterText); @@ -552,7 +560,7 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() } // Help text with breathing animation - const char* helpText = "Press Escape or Enter to continue"; + const char* helpText = T("menu.setup.press_to_close", "Press Escape or Enter to continue"); ImGui::SetWindowFontScale(HELP_TEXT_SCALE); centerText(helpText); diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 27ec1a1703..5b48a79131 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -5,6 +5,7 @@ #include "Fonts.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Plugin.h" #include "ShaderCache.h" #include "State.h" @@ -161,43 +162,43 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow if (ImGui::BeginTable("##ActionButtons", 4, ImGuiTableFlags_SizingStretchSame)) { // Save Settings Button ImGui::TableNextColumn(); - if (Util::ButtonWithFlash("Save Settings", { -1, 0 })) { + if (Util::ButtonWithFlash(T("menu.save_settings", "Save Settings"), { -1, 0 })) { globals::state->Save(); globals::state->SaveTheme(); } // Restore Saved Settings Button ImGui::TableNextColumn(); - if (ImGui::Button("Restore Saved Settings", { -1, 0 })) { + if (ImGui::Button(T("menu.restore_settings", "Restore Saved Settings"), { -1, 0 })) { globals::state->Load(); } // Clear Shader Cache Button ImGui::TableNextColumn(); - if (ImGui::Button("Clear Shader Cache", { -1, 0 })) { + if (ImGui::Button(T("menu.clear_shader_cache", "Clear Shader Cache"), { -1, 0 })) { Util::RequestClearShaderCacheConfirmation(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Clears the shader cache and disk cache (if enabled). " - "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " - "The Disk Cache is a collection of compiled shaders on disk. " - "Clearing will mean that shaders are recompiled only when the game re-encounters them. "); + ImGui::Text("%s", T("menu.clear_shader_cache_tooltip", + "Clears the shader cache and disk cache (if enabled). " + "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " + "The Disk Cache is a collection of compiled shaders on disk. " + "Clearing will mean that shaders are recompiled only when the game re-encounters them.")); } // Error message toggle if needed if (shaderCache->GetFailedTasks()) { ImGui::TableNextRow(); ImGui::TableNextColumn(); - if (ImGui::Button("Toggle Error Message", { -1, 0 })) { + if (ImGui::Button(T("menu.toggle_error_message", "Toggle Error Message"), { -1, 0 })) { shaderCache->ToggleErrorMessages(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Hide or show the shader failure message. " - "Your installation is broken and will likely see errors in game. " - "Please double check you have updated all features and that your load order is correct. " - "See CommunityShaders.log for details and check the Nexus Mods page or Discord server. "); + ImGui::Text("%s", T("menu.toggle_error_message_tooltip", + "Hide or show the shader failure message. " + "Your installation is broken and will likely see errors in game. " + "Please double check you have updated all features and that your load order is correct. " + "See CommunityShaders.log for details and check the Nexus Mods page or Discord server.")); } } @@ -213,15 +214,15 @@ void MenuHeaderRenderer::RenderHeader(bool isDocked, bool showLogo, bool canShow } else if (shaderCache->GetFailedTasks() && !isDocked) { // If icons are enabled but there are failed tasks, show error toggle button // and add the second separator (only when not docked) - if (ImGui::Button("Toggle Error Message", { -1, 0 })) { + if (ImGui::Button(T("menu.toggle_error_message", "Toggle Error Message"), { -1, 0 })) { shaderCache->ToggleErrorMessages(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Hide or show the shader failure message. " - "Your installation is broken and will likely see errors in game. " - "Please double check you have updated all features and that your load order is correct. " - "See CommunityShaders.log for details and check the Nexus Mods page or Discord server. "); + ImGui::Text("%s", T("menu.toggle_error_message_tooltip", + "Hide or show the shader failure message. " + "Your installation is broken and will likely see errors in game. " + "Please double check you have updated all features and that your load order is correct. " + "See CommunityShaders.log for details and check the Nexus Mods page or Discord server.")); } // Add second separator when showing error button @@ -242,7 +243,7 @@ std::vector MenuHeaderRenderer::BuildActionIcons // Build list of available action icons (in display order) if (uiIcons.saveSettings.texture) { actionIcons.push_back({ uiIcons.saveSettings.texture, - "Save Settings", + T("menu.save_settings", "Save Settings"), []() { globals::state->Save(); globals::state->SaveTheme(); @@ -250,19 +251,18 @@ std::vector MenuHeaderRenderer::BuildActionIcons } if (uiIcons.loadSettings.texture) { actionIcons.push_back({ uiIcons.loadSettings.texture, - "Restore Saved Settings", + T("menu.restore_settings", "Restore Saved Settings"), []() { globals::state->Load(); } }); } if (uiIcons.clearCache.texture) { actionIcons.push_back({ uiIcons.clearCache.texture, - "Clear Shader Cache\n\n" - "Clears the shader cache and disk cache (if enabled).\n" - "The Shader Cache is the collection of compiled shaders which replace\n" - "the vanilla shaders at runtime. The Disk Cache is a collection of\n" - "compiled shaders on disk. Clearing will mean that shaders are\n" - "recompiled only when the game re-encounters them.", + T("menu.clear_shader_cache_tooltip", + "Clears the shader cache and disk cache (if enabled). " + "The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. " + "The Disk Cache is a collection of compiled shaders on disk. " + "Clearing will mean that shaders are recompiled only when the game re-encounters them."), []() { Util::RequestClearShaderCacheConfirmation(); } }); diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index d399339e05..3f31d5ca8d 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -10,15 +10,16 @@ #include #include +#include "CSEditor/EditorWindow.h" #include "Feature.h" #include "FeatureIssues.h" #include "Features/RenderDoc.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" #include "Util.h" -#include "CSEditor/EditorWindow.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" @@ -40,7 +41,7 @@ namespace static_cast(failed)); if (FeatureIssues::HasPotentialShaderModifyingFeatures()) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "Features that may have modified shaders detected. Check Feature Issues in the Menu."); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", T("overlay.modified_features", "Features that may have modified shaders detected. Check Feature Issues in the Menu.")); } } @@ -301,7 +302,7 @@ void OverlayRenderer::RenderShaderCompilationStatus(const std::functionGetSettings().SkipCompilationKey)); ImGui::TextUnformatted(skipShadersText.c_str()); - ImGui::TextUnformatted("WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading."); + ImGui::TextUnformatted(T("overlay.uncompiled_warning", "WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading.")); } if (failed && !hide) { DrawShaderCompilationFailures(failed, themeSettings); @@ -428,7 +429,7 @@ void OverlayRenderer::RenderShaderBlockingStatus() return; } - Util::Text::Error("Shader Blocking Active"); + Util::Text::Error(T("overlay.shader_blocking_active", "Shader Blocking Active")); ImGui::Text("Blocked: %s", shaderCache->blockedKey.c_str()); // Try to get more details from active shaders diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index a0322dbeb7..b5bce28b15 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1,5 +1,6 @@ #include "SettingsTabRenderer.h" +#include #include #include #include @@ -9,6 +10,7 @@ #include "Features/VR.h" #include "Fonts.h" #include "Globals.h" +#include "I18n/I18n.h" #include "IconLoader.h" #include "Menu.h" #include "ShaderCache.h" @@ -22,134 +24,143 @@ namespace { using FontRoleGuard = MenuFonts::FontRoleGuard; // Convenience alias + std::string GetLocaleDisplayLabel(std::string_view localeCode, std::string_view metadataName) + { + if (!metadataName.empty()) { + return std::string(metadataName); + } + + return std::string(localeCode); + } + // Convert ImGui internal color names to user-friendly display names const char* GetFriendlyColorName(int colorIndex) { switch (colorIndex) { case ImGuiCol_Text: - return "Text"; + return T("menu.settings.color_text", "Text"); case ImGuiCol_TextDisabled: - return "Text (Disabled)"; + return T("menu.settings.color_text_disabled", "Text (Disabled)"); case ImGuiCol_WindowBg: - return "Window Background"; + return T("menu.settings.color_window_bg", "Window Background"); case ImGuiCol_ChildBg: - return "Child Window Background"; + return T("menu.settings.color_child_bg", "Child Window Background"); case ImGuiCol_PopupBg: - return "Popup Background"; + return T("menu.settings.color_popup_bg", "Popup Background"); case ImGuiCol_Border: - return "Border"; + return T("menu.settings.color_border", "Border"); case ImGuiCol_BorderShadow: - return "Border Shadow"; + return T("menu.settings.color_border_shadow", "Border Shadow"); case ImGuiCol_FrameBg: - return "Frame Background"; + return T("menu.settings.color_frame_bg", "Frame Background"); case ImGuiCol_FrameBgHovered: - return "Frame Background (Hovered)"; + return T("menu.settings.color_frame_bg_hovered", "Frame Background (Hovered)"); case ImGuiCol_FrameBgActive: - return "Frame Background (Active)"; + return T("menu.settings.color_frame_bg_active", "Frame Background (Active)"); case ImGuiCol_TitleBg: - return "Title Bar Background"; + return T("menu.settings.color_title_bg", "Title Bar Background"); case ImGuiCol_TitleBgActive: - return "Title Bar Background (Active)"; + return T("menu.settings.color_title_bg_active", "Title Bar Background (Active)"); case ImGuiCol_TitleBgCollapsed: - return "Title Bar Background (Collapsed)"; + return T("menu.settings.color_title_bg_collapsed", "Title Bar Background (Collapsed)"); case ImGuiCol_MenuBarBg: - return "Menu Bar Background"; + return T("menu.settings.color_menu_bar_bg", "Menu Bar Background"); case ImGuiCol_ScrollbarBg: - return "Scrollbar Background"; + return T("menu.settings.color_scrollbar_bg", "Scrollbar Background"); case ImGuiCol_ScrollbarGrab: - return "Scrollbar Grab"; + return T("menu.settings.color_scrollbar_grab", "Scrollbar Grab"); case ImGuiCol_ScrollbarGrabHovered: - return "Scrollbar Grab (Hovered)"; + return T("menu.settings.color_scrollbar_grab_hovered", "Scrollbar Grab (Hovered)"); case ImGuiCol_ScrollbarGrabActive: - return "Scrollbar Grab (Active)"; + return T("menu.settings.color_scrollbar_grab_active", "Scrollbar Grab (Active)"); case ImGuiCol_CheckMark: - return "Checkbox Checkmark"; + return T("menu.settings.color_check_mark", "Checkbox Checkmark"); case ImGuiCol_SliderGrab: - return "Slider Grab"; + return T("menu.settings.color_slider_grab", "Slider Grab"); case ImGuiCol_SliderGrabActive: - return "Slider Grab (Active)"; + return T("menu.settings.color_slider_grab_active", "Slider Grab (Active)"); case ImGuiCol_Button: - return "Button"; + return T("menu.settings.color_button", "Button"); case ImGuiCol_ButtonHovered: - return "Button (Hovered)"; + return T("menu.settings.color_button_hovered", "Button (Hovered)"); case ImGuiCol_ButtonActive: - return "Button (Active)"; + return T("menu.settings.color_button_active", "Button (Active)"); case ImGuiCol_Header: - return "Header"; + return T("menu.settings.color_header", "Header"); case ImGuiCol_HeaderHovered: - return "Header (Hovered)"; + return T("menu.settings.color_header_hovered", "Header (Hovered)"); case ImGuiCol_HeaderActive: - return "Header (Active)"; + return T("menu.settings.color_header_active", "Header (Active)"); case ImGuiCol_Separator: - return "Separator"; + return T("menu.settings.color_separator", "Separator"); case ImGuiCol_SeparatorHovered: - return "Separator (Hovered)"; + return T("menu.settings.color_separator_hovered", "Separator (Hovered)"); case ImGuiCol_SeparatorActive: - return "Separator (Active)"; + return T("menu.settings.color_separator_active", "Separator (Active)"); case ImGuiCol_ResizeGrip: - return "Resize Grip"; + return T("menu.settings.color_resize_grip", "Resize Grip"); case ImGuiCol_ResizeGripHovered: - return "Resize Grip (Hovered)"; + return T("menu.settings.color_resize_grip_hovered", "Resize Grip (Hovered)"); case ImGuiCol_ResizeGripActive: - return "Resize Grip (Active)"; + return T("menu.settings.color_resize_grip_active", "Resize Grip (Active)"); case ImGuiCol_InputTextCursor: - return "Input Text Cursor"; + return T("menu.settings.color_input_text_cursor", "Input Text Cursor"); case ImGuiCol_Tab: - return "Tab"; + return T("menu.settings.color_tab", "Tab"); case ImGuiCol_TabHovered: - return "Tab (Hovered)"; + return T("menu.settings.color_tab_hovered", "Tab (Hovered)"); case ImGuiCol_TabSelected: - return "Tab (Selected)"; + return T("menu.settings.color_tab_selected", "Tab (Selected)"); case ImGuiCol_TabSelectedOverline: - return "Tab Selected Overline"; + return T("menu.settings.color_tab_selected_overline", "Tab Selected Overline"); case ImGuiCol_TabDimmed: - return "Tab (Dimmed)"; + return T("menu.settings.color_tab_dimmed", "Tab (Dimmed)"); case ImGuiCol_TabDimmedSelected: - return "Tab (Dimmed Selected)"; + return T("menu.settings.color_tab_dimmed_selected", "Tab (Dimmed Selected)"); case ImGuiCol_TabDimmedSelectedOverline: - return "Tab Dimmed Selected Overline"; + return T("menu.settings.color_tab_dimmed_selected_overline", "Tab Dimmed Selected Overline"); case ImGuiCol_DockingPreview: - return "Docking Preview"; + return T("menu.settings.color_docking_preview", "Docking Preview"); case ImGuiCol_DockingEmptyBg: - return "Docking Empty Background"; + return T("menu.settings.color_docking_empty_bg", "Docking Empty Background"); case ImGuiCol_PlotLines: - return "Plot Lines"; + return T("menu.settings.color_plot_lines", "Plot Lines"); case ImGuiCol_PlotLinesHovered: - return "Plot Lines (Hovered)"; + return T("menu.settings.color_plot_lines_hovered", "Plot Lines (Hovered)"); case ImGuiCol_PlotHistogram: - return "Plot Histogram"; + return T("menu.settings.color_plot_histogram", "Plot Histogram"); case ImGuiCol_PlotHistogramHovered: - return "Plot Histogram (Hovered)"; + return T("menu.settings.color_plot_histogram_hovered", "Plot Histogram (Hovered)"); case ImGuiCol_TableHeaderBg: - return "Table Header Background"; + return T("menu.settings.color_table_header_bg", "Table Header Background"); case ImGuiCol_TableBorderStrong: - return "Table Border (Strong)"; + return T("menu.settings.color_table_border_strong", "Table Border (Strong)"); case ImGuiCol_TableBorderLight: - return "Table Border (Light)"; + return T("menu.settings.color_table_border_light", "Table Border (Light)"); case ImGuiCol_TableRowBg: - return "Table Row Background"; + return T("menu.settings.color_table_row_bg", "Table Row Background"); case ImGuiCol_TableRowBgAlt: - return "Table Row Background (Alternate)"; + return T("menu.settings.color_table_row_bg_alt", "Table Row Background (Alternate)"); case ImGuiCol_TextLink: - return "Text Link"; + return T("menu.settings.color_text_link", "Text Link"); case ImGuiCol_TextSelectedBg: - return "Text Selection Background"; + return T("menu.settings.color_text_selected_bg", "Text Selection Background"); case ImGuiCol_TreeLines: - return "Tree Lines"; + return T("menu.settings.color_tree_lines", "Tree Lines"); case ImGuiCol_DragDropTarget: - return "Drag & Drop Target"; + return T("menu.settings.color_drag_drop_target", "Drag & Drop Target"); case ImGuiCol_DragDropTargetBg: - return "Drag & Drop Target Background"; + return T("menu.settings.color_drag_drop_target_bg", "Drag & Drop Target Background"); case ImGuiCol_UnsavedMarker: - return "Unsaved Marker"; + return T("menu.settings.color_unsaved_marker", "Unsaved Marker"); case ImGuiCol_NavCursor: - return "Navigation Cursor"; + return T("menu.settings.color_nav_cursor", "Navigation Cursor"); case ImGuiCol_NavWindowingHighlight: - return "Window Navigation Highlight"; + return T("menu.settings.color_nav_windowing_highlight", "Window Navigation Highlight"); case ImGuiCol_NavWindowingDimBg: - return "Window Navigation Dim Background"; + return T("menu.settings.color_nav_windowing_dim_bg", "Window Navigation Dim Background"); case ImGuiCol_ModalWindowDimBg: - return "Modal Window Dim Background"; + return T("menu.settings.color_modal_window_dim_bg", "Modal Window Dim Background"); default: return ImGui::GetStyleColorName(colorIndex); } @@ -187,7 +198,8 @@ namespace { auto& ts = globals::menu->GetSettings().Theme; ImGui::PushStyleColor(ImGuiCol_Text, ts.StatusPalette.InfoColor); - ImGui::TextWrapped("Theme changes are not saved with the global \"Save Settings\" button. Use the Themes tab to save changes to this theme."); + ImGui::TextWrapped("%s", T("menu.settings.theme_save_info", + "Theme changes are not saved with the global \"Save Settings\" button. Use the Themes tab to save changes to this theme.")); ImGui::PopStyleColor(); ImGui::Spacing(); } @@ -206,61 +218,62 @@ void SettingsTabRenderer::RenderGeneralSettings(SettingsState& state) void SettingsTabRenderer::RenderShadersTab() { - if (BeginTabItemWithFont("Shaders", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_shaders", "Shaders"), "GeneralShadersTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto shaderCache = globals::shaderCache; bool useCustomShaders = shaderCache->IsEnabled(); - if (ImGui::Checkbox("Use Custom Shaders", &useCustomShaders)) { + if (ImGui::Checkbox(T("menu.settings.use_custom_shaders", "Use Custom Shaders"), &useCustomShaders)) { shaderCache->SetEnabled(useCustomShaders); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disabling this effectively disables all features."); + ImGui::Text("%s", T("menu.settings.use_custom_shaders_tooltip", "Disabling this effectively disables all features.")); } bool useDiskCache = shaderCache->IsDiskCache(); - if (ImGui::Checkbox("Enable Disk Cache", &useDiskCache)) { + if (ImGui::Checkbox(T("menu.settings.enable_disk_cache", "Enable Disk Cache"), &useDiskCache)) { shaderCache->SetDiskCache(useDiskCache); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disables loading shaders from disk and prevents saving compiled shaders to disk cache."); + ImGui::Text("%s", T("menu.settings.enable_disk_cache_tooltip", "Disables loading shaders from disk and prevents saving compiled shaders to disk cache.")); } bool skipUnchanged = shaderCache->IsSkipUnchangedShaders(); ImGui::BeginDisabled(!useDiskCache); - if (ImGui::Checkbox("Skip Unchanged Shaders", &skipUnchanged)) { + if (ImGui::Checkbox(T("menu.settings.skip_unchanged_shaders", "Skip Unchanged Shaders"), &skipUnchanged)) { shaderCache->SetSkipUnchangedShaders(skipUnchanged); } ImGui::EndDisabled(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "When enabled, each shader is recompiled from source only if its .hlsl file " - "is newer than the cached .bin on disk. " - "Shaders whose source has not changed are loaded directly from the disk cache, " - "avoiding the full startup compilation cost. " - "Useful for iterative testing: change a shader file and only that shader is rebuilt. " - "Requires 'Enable Disk Cache' to be active."); + ImGui::Text("%s", T("menu.settings.skip_unchanged_shaders_tooltip", + "When enabled, each shader is recompiled from source only if its .hlsl file " + "is newer than the cached .bin on disk. " + "Shaders whose source has not changed are loaded directly from the disk cache, " + "avoiding the full startup compilation cost. " + "Useful for iterative testing: change a shader file and only that shader is rebuilt. " + "Requires 'Enable Disk Cache' to be active.")); } bool useAsync = shaderCache->IsAsync(); - if (ImGui::Checkbox("Enable Async", &useAsync)) { + if (ImGui::Checkbox(T("menu.settings.enable_async", "Enable Async"), &useAsync)) { shaderCache->SetAsync(useAsync); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!"); + ImGui::Text("%s", T("menu.settings.enable_async_tooltip", "Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!")); } // Skip confirmation when clearing shader cache auto& menuSettings = globals::menu->GetSettings(); bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; - if (ImGui::Checkbox("Skip Clear Cache Dialogue", &skipConfirmation)) { + if (ImGui::Checkbox(T("menu.settings.skip_clear_cache_dialogue", "Skip Clear Cache Dialogue"), &skipConfirmation)) { menuSettings.SkipClearCacheConfirmation = skipConfirmation; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When checked, the shader cache will be cleared immediately without asking for confirmation."); + ImGui::Text("%s", T("menu.settings.skip_clear_cache_dialogue_tooltip", "When checked, the shader cache will be cleared immediately without asking for confirmation.")); } if (shaderCache->GetTotalTasks() > 0) { - ImGui::Text("Last shader cache build duration: %s", + ImGui::Text(T("menu.settings.last_shader_cache_duration", "Last shader cache build duration: %s"), shaderCache->GetShaderStatsString(true, true).c_str()); // Stacked bar showing compilation breakdown @@ -285,12 +298,12 @@ void SettingsTabRenderer::RenderShadersTab() const char* label; }; Segment segments[] = { - { cacheHits, IM_COL32(120, 120, 120, 255), "Deduplicated" }, - { diskHits, IM_COL32(70, 130, 200, 255), "Disk cache" }, - { fast, IM_COL32(80, 180, 80, 255), "Fast (<2s)" }, - { medium, IM_COL32(220, 180, 50, 255), "Slow (2-8s)" }, - { verySlow, IM_COL32(220, 60, 60, 255), "Very slow (>=8s)" }, - { failed, IM_COL32(160, 30, 30, 255), "Failed" }, + { cacheHits, IM_COL32(120, 120, 120, 255), T("menu.settings.shader_deduplicated", "Deduplicated") }, + { diskHits, IM_COL32(70, 130, 200, 255), T("menu.settings.shader_disk_cache", "Disk cache") }, + { fast, IM_COL32(80, 180, 80, 255), T("menu.settings.shader_fast", "Fast (<2s)") }, + { medium, IM_COL32(220, 180, 50, 255), T("menu.settings.shader_slow", "Slow (2-8s)") }, + { verySlow, IM_COL32(220, 60, 60, 255), T("menu.settings.shader_very_slow", "Very slow (>=8s)") }, + { failed, IM_COL32(160, 30, 30, 255), T("menu.settings.shader_failed", "Failed") }, }; float barHeight = 14.0f * Util::GetUIScale(); @@ -344,41 +357,42 @@ void SettingsTabRenderer::RenderShadersTab() void SettingsTabRenderer::RenderKeybindingsTab( SettingsState& state) { - if (BeginTabItemWithFont("Keybindings", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_keybindings", "Keybindings"), "GeneralKeybindingsTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto& settings = globals::menu->GetSettings(); Util::InputComboWidget( - "Toggle Key:", + T("menu.settings.toggle_key", "Toggle Key:"), settings.ToggleKey, state.settingToggleKey, "Change##toggle"); Util::InputComboWidget( - "Effect Toggle Key:", + T("menu.settings.effect_toggle_key", "Effect Toggle Key:"), settings.EffectToggleKey, state.settingsEffectsToggle, "Change##EffectToggle"); Util::InputComboWidget( - "Skip Compilation Key:", + T("menu.settings.skip_compilation_key", "Skip Compilation Key:"), settings.SkipCompilationKey, state.settingSkipCompilationKey, "Change##skip"); Util::InputComboWidget( - "Overlay Toggle Key:", + T("menu.settings.overlay_toggle_key", "Overlay Toggle Key:"), settings.OverlayToggleKey, state.settingOverlayToggleKey, "Change##OverlayToggle"); Util::InputComboWidget( - "CS Editor Toggle Key:", + T("menu.settings.cs_editor_toggle_key", "CS Editor Toggle Key:"), settings.CSEditorToggleKey, state.settingCSEditorToggleKey, "Change##CSEditorToggle"); Util::InputComboWidget( - "Screenshot Key:", + T("menu.settings.screenshot_key", "Screenshot Key:"), settings.ScreenshotKey, state.settingScreenshotKey, "Change##Screenshot"); @@ -389,7 +403,8 @@ void SettingsTabRenderer::RenderKeybindingsTab( void SettingsTabRenderer::RenderInterfaceTab() { - if (BeginTabItemWithFont("Interface", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_interface", "Interface"), "GeneralInterfaceTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { MenuFonts::TabBarPaddingGuard tabPaddingGuard(Menu::FontRole::Subheading); if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { RenderBehaviorTab(); @@ -405,71 +420,109 @@ void SettingsTabRenderer::RenderInterfaceTab() void SettingsTabRenderer::RenderBehaviorTab() { - if (BeginTabItemWithFont("Behavior", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_behavior", "Behavior"), "InterfaceBehaviorTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; RenderSaveInfoText(); - SeparatorTextWithFont("UI Behavior", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.section_language", "Language"), Menu::FontRole::Subheading); + + { + auto* i18n = I18n::GetSingleton(); + auto locales = i18n->GetAvailableLocales(); + auto currentLocale = i18n->GetCurrentLocale(); + + // Find the display name for the current locale + std::string currentDisplayName = currentLocale; + for (const auto& [code, name] : locales) { + if (code == currentLocale) { + currentDisplayName = GetLocaleDisplayLabel(code, name); + break; + } + } + + if (ImGui::BeginCombo(T("menu.settings.language", "Language"), currentDisplayName.c_str())) { + for (const auto& [code, name] : locales) { + bool isSelected = (code == currentLocale); + auto displayName = GetLocaleDisplayLabel(code, name); + ImGui::PushID(code.c_str()); + if (ImGui::Selectable(displayName.c_str(), isSelected)) { + i18n->SetLocale(code); + globals::menu->pendingFontReload = true; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("menu.settings.language_tooltip", "Select the display language for the Community Shaders interface.")); + } + } + + SeparatorTextWithFont(T("menu.settings.ui_behavior", "UI Behavior"), Menu::FontRole::Subheading); - ImGui::Checkbox("Show Icon Buttons in Header", &themeSettings.ShowActionIcons); + ImGui::Checkbox(T("menu.settings.show_icon_buttons_in_header", "Show Icon Buttons in Header"), &themeSettings.ShowActionIcons); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\n" - "When disabled: Shows as text buttons below the header"); + ImGui::Text("%s", T("menu.settings.show_icon_buttons_in_header_tooltip", + "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\n" + "When disabled: Shows as text buttons below the header")); } if (themeSettings.ShowActionIcons) { ImGui::Indent(); - if (ImGui::Checkbox("Use Monochrome Icons", &themeSettings.UseMonochromeIcons)) { + if (ImGui::Checkbox(T("menu.settings.use_monochrome_icons", "Use Monochrome Icons"), &themeSettings.UseMonochromeIcons)) { globals::menu->pendingIconReload = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Uses white monochrome icons that adapt to your theme's text color"); + ImGui::Text("%s", T("menu.settings.use_monochrome_icons_tooltip", "Uses white monochrome icons that adapt to your theme's text color")); } ImGui::SameLine(); - if (ImGui::Checkbox("Use Monochrome CS Logo", &themeSettings.UseMonochromeLogo)) { + if (ImGui::Checkbox(T("menu.settings.use_monochrome_cs_logo", "Use Monochrome CS Logo"), &themeSettings.UseMonochromeLogo)) { globals::menu->pendingIconReload = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Uses monochrome version of the Community Shaders logo"); + ImGui::Text("%s", T("menu.settings.use_monochrome_cs_logo_tooltip", "Uses monochrome version of the Community Shaders logo")); } ImGui::Unindent(); } - ImGui::Checkbox("Show Footer", &themeSettings.ShowFooter); + ImGui::Checkbox(T("menu.settings.show_footer", "Show Footer"), &themeSettings.ShowFooter); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Shows the footer with game version, swap chain, and GPU information at the bottom of the window"); + ImGui::Text("%s", T("menu.settings.show_footer_tooltip", "Shows the footer with game version, swap chain, and GPU information at the bottom of the window")); } - ImGui::Checkbox("Center Header Title", &themeSettings.CenterHeader); + ImGui::Checkbox(T("menu.settings.center_header_title", "Center Header Title"), &themeSettings.CenterHeader); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Centers the Community Shaders title and logo in the header title bar"); + ImGui::Text("%s", T("menu.settings.center_header_title_tooltip", "Centers the Community Shaders title and logo in the header title bar")); } - ImGui::Checkbox("Auto-hide Feature List", &globals::menu->GetSettings().AutoHideFeatureList); + ImGui::Checkbox(T("menu.settings.auto_hide_feature_list", "Auto-hide Feature List"), &globals::menu->GetSettings().AutoHideFeatureList); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Automatically hides the left feature list panel. Move cursor to the left edge to show it."); + ImGui::Text("%s", T("menu.settings.auto_hide_feature_list_tooltip", "Automatically hides the left feature list panel. Move cursor to the left edge to show it.")); } - if (ImGui::Checkbox("Require Shift to Dock", &globals::menu->GetSettings().RequireShiftToDock)) { + if (ImGui::Checkbox(T("menu.settings.require_shift_to_dock", "Require Shift to Dock"), &globals::menu->GetSettings().RequireShiftToDock)) { ImGui::GetIO().ConfigDockingWithShift = globals::menu->GetSettings().RequireShiftToDock; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When enabled, you must hold Shift while dragging to dock/snap windows. Prevents accidental docking."); + ImGui::Text("%s", T("menu.settings.require_shift_to_dock_tooltip", "When enabled, you must hold Shift while dragging to dock/snap windows. Prevents accidental docking.")); } - ImGui::SliderFloat("Tooltip Hover Delay", &themeSettings.TooltipHoverDelay, 0.0f, 2.0f, "%.2f s", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T("menu.settings.tooltip_hover_delay", "Tooltip Hover Delay"), &themeSettings.TooltipHoverDelay, 0.0f, 2.0f, "%.2f s", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); + ImGui::TextUnformatted(T("menu.settings.tooltip_hover_delay_tooltip", "Time in seconds to wait before a tooltip appears when hovering over an item.")); } - SeparatorTextWithFont("Visual Effects", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.visual_effects", "Visual Effects"), Menu::FontRole::Subheading); - if (ImGui::Checkbox("Background Blur", &themeSettings.BackgroundBlurEnabled)) { + if (ImGui::Checkbox(T("menu.settings.background_blur", "Background Blur"), &themeSettings.BackgroundBlurEnabled)) { BackgroundBlur::SetEnabled(themeSettings.BackgroundBlurEnabled); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Applies a blur effect to the background behind the menu window."); + ImGui::Text("%s", T("menu.settings.background_blur_tooltip", "Applies a blur effect to the background behind the menu window.")); } ImGui::EndTabItem(); @@ -478,11 +531,15 @@ void SettingsTabRenderer::RenderBehaviorTab() void SettingsTabRenderer::RenderThemesTab() { - if (BeginTabItemWithFont("Themes", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_themes", "Themes"), "InterfaceThemesTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; // Static variables for popup state and new theme creation - static Util::ConfirmationPopup deleteThemePopup("Delete Theme", "", "Delete", "Cancel"); + static Util::ConfirmationPopup deleteThemePopup( + T("menu.settings.delete_theme_title", "Delete Theme"), "", + T("menu.settings.delete_button", "Delete"), + T("menu.settings.cancel", "Cancel")); static bool showCreateThemePopup = false; static char newThemeName[128] = ""; static char newThemeDisplayName[128] = ""; @@ -501,7 +558,7 @@ void SettingsTabRenderer::RenderThemesTab() static bool updateSuccess = false; // Theme Preset Selection - SeparatorTextWithFont("Theme Preset", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.theme_preset", "Theme Preset"), Menu::FontRole::Subheading); // Get theme manager auto themeManager = ThemeManager::GetSingleton(); @@ -557,7 +614,7 @@ void SettingsTabRenderer::RenderThemesTab() } } - if (ImGui::Button("Refresh")) { + if (ImGui::Button(T("menu.settings.refresh", "Refresh"))) { themeManager->RefreshThemes(); // Ensure a valid theme is still selected const auto* themeInfo = themeManager->GetThemeInfo(currentThemePreset); @@ -575,17 +632,17 @@ void SettingsTabRenderer::RenderThemesTab() } ImGui::SameLine(); - if (ImGui::Button("Open Themes Folder")) { + if (ImGui::Button(T("menu.settings.open_themes_folder", "Open Themes Folder"))) { std::filesystem::path themesPath = Util::PathHelpers::GetThemesRealPath(); ShellExecuteA(NULL, "open", themesPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Opens the Themes folder where you can add custom theme files."); + ImGui::Text("%s", T("menu.settings.open_themes_folder_tooltip", "Opens the Themes folder where you can add custom theme files.")); } ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.InfoColor); - ImGui::TextWrapped("If you changed the theme above, save your selection using the global \"Save Settings\" button."); + ImGui::TextWrapped("%s", T("menu.settings.theme_save_reminder", "If you changed the theme above, save your selection using the global \"Save Settings\" button.")); ImGui::PopStyleColor(); // Selected theme section: name + description @@ -594,7 +651,7 @@ void SettingsTabRenderer::RenderThemesTab() if (currentItem >= 0 && currentItem < static_cast(themes.size())) { ImGui::Spacing(); const auto& selectedTheme = themes[currentItem]; - ImGui::Text("Selected Theme: "); + ImGui::Text("%s", T("menu.settings.selected_theme", "Selected Theme: ")); ImGui::SameLine(0, 0); ImGui::TextColored(themeSettings.StatusPalette.InfoColor, "%s", selectedTheme.displayName.c_str()); if (!selectedTheme.description.empty()) { @@ -607,7 +664,7 @@ void SettingsTabRenderer::RenderThemesTab() const auto* currentThemeInfo = themeManager->GetThemeInfo(currentThemePreset); if (!isPreset) { - if (Util::ButtonWithFlash("Save")) { + if (Util::ButtonWithFlash(T("menu.settings.save_theme_button", "Save"))) { if (currentThemeInfo) { // Get current settings json currentThemeJson; @@ -666,13 +723,13 @@ void SettingsTabRenderer::RenderThemesTab() } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Updates the currently selected theme (%s) with your current settings", currentThemePreset.c_str()); + ImGui::Text(T("menu.settings.save_theme_tooltip", "Updates the currently selected theme (%s) with your current settings"), currentThemePreset.c_str()); } ImGui::SameLine(); } - if (Util::ButtonWithFlash("Save As New Theme")) { + if (Util::ButtonWithFlash(T("menu.settings.save_as_new_theme", "Save As New Theme"))) { showCreateThemePopup = true; memset(newThemeName, 0, sizeof(newThemeName)); memset(newThemeDisplayName, 0, sizeof(newThemeDisplayName)); @@ -682,15 +739,17 @@ void SettingsTabRenderer::RenderThemesTab() if (!isPreset && currentThemeInfo && !currentThemeInfo->filePath.empty()) { ImGui::SameLine(); - if (Util::ErrorButtonWithFlash("Delete")) { + if (Util::ErrorButtonWithFlash(T("menu.settings.delete_theme", "Delete"))) { deleteThemePopup.message = - "Are you sure you want to delete the theme '" + + std::string(T("menu.settings.delete_theme_confirm_part1", + "Are you sure you want to delete the theme '")) + (currentThemeInfo->displayName.empty() ? currentThemePreset : currentThemeInfo->displayName) + - "'?\n\nThis will permanently remove the theme file. This cannot be undone."; + T("menu.settings.delete_theme_confirm_part2", + "'?\n\nThis will permanently remove the theme file. This cannot be undone."); deleteThemePopup.Request(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Delete the theme file for '%s'. This cannot be undone.", + ImGui::Text(T("menu.settings.delete_theme_tooltip", "Delete the theme file for '%s'. This cannot be undone."), (currentThemeInfo->displayName.empty() ? currentThemePreset : currentThemeInfo->displayName).c_str()); } } @@ -702,9 +761,9 @@ void SettingsTabRenderer::RenderThemesTab() if (updateSuccess) { if (changedSettings.empty()) { - ImGui::TextColored(themeSettings.StatusPalette.SuccessColor, "Theme updated successfully - no changes detected"); + ImGui::TextColored(themeSettings.StatusPalette.SuccessColor, "%s", T("menu.settings.theme_updated_no_changes", "Theme updated successfully - no changes detected")); } else { - ImGui::TextColored(themeSettings.StatusPalette.SuccessColor, "Theme updated successfully! Changed settings:"); + ImGui::TextColored(themeSettings.StatusPalette.SuccessColor, "%s", T("menu.settings.theme_updated_with_changes", "Theme updated successfully! Changed settings:")); ImGui::Indent(); for (const auto& change : changedSettings) { ImGui::BulletText("%s: %s -> %s", change.path.c_str(), change.oldValue.c_str(), change.newValue.c_str()); @@ -712,7 +771,7 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::Unindent(); } } else { - ImGui::TextColored(themeSettings.StatusPalette.Error, "Failed to update theme"); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", T("menu.settings.theme_update_failed", "Failed to update theme")); } ImGui::Separator(); @@ -720,12 +779,12 @@ void SettingsTabRenderer::RenderThemesTab() // Create Theme Popup if (showCreateThemePopup) { - ImGui::OpenPopup("Create New Theme"); + ImGui::OpenPopup(T("menu.settings.create_new_theme", "Create New Theme")); } // Popup modal for creating new theme - if (auto popup = Util::CenteredPopupModal("Create New Theme", &showCreateThemePopup)) { - ImGui::Text("Create a new theme with your current settings:"); + if (auto popup = Util::CenteredPopupModal(T("menu.settings.create_new_theme", "Create New Theme"), &showCreateThemePopup)) { + ImGui::Text("%s", T("menu.settings.create_new_theme_hint", "Create a new theme with your current settings:")); ImGui::Separator(); auto safeNewThemeName = Util::FileHelpers::SanitizeFileName(newThemeName); @@ -749,7 +808,7 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); } - ImGui::InputText("Theme Name", newThemeName, sizeof(newThemeName)); + ImGui::InputText(T("menu.settings.theme_name", "Theme Name"), newThemeName, sizeof(newThemeName)); if (isThemeNameError && showValidationError) { ImGui::PopStyleVar(); @@ -759,14 +818,14 @@ void SettingsTabRenderer::RenderThemesTab() // Show inline error message if (showValidationError) { if (isThemeNameEmpty) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "Theme name is required"); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", T("menu.settings.theme_name_required", "Theme name is required")); } else if (isDuplicateName) { - ImGui::TextColored(themeSettings.StatusPalette.Error, "A theme with this name already exists"); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", T("menu.settings.theme_name_duplicate", "A theme with this name already exists")); } } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("File name for the theme (without .json extension)"); + ImGui::Text("%s", T("menu.settings.theme_name_tooltip", "File name for the theme (without .json extension)")); } // Highlight the input field if invalid and validation error is shown @@ -775,30 +834,30 @@ void SettingsTabRenderer::RenderThemesTab() ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); } - ImGui::InputText("Display Name", newThemeDisplayName, sizeof(newThemeDisplayName)); + ImGui::InputText(T("menu.settings.display_name", "Display Name"), newThemeDisplayName, sizeof(newThemeDisplayName)); if (isDuplicateDisplayName && showValidationError) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); - ImGui::TextColored(themeSettings.StatusPalette.Error, "A theme with this display name already exists"); + ImGui::TextColored(themeSettings.StatusPalette.Error, "%s", T("menu.settings.display_name_duplicate", "A theme with this display name already exists")); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Human-readable name shown in the dropdown"); + ImGui::Text("%s", T("menu.settings.display_name_tooltip", "Human-readable name shown in the dropdown")); } { float scale = Util::GetUIScale(); - ImGui::InputTextMultiline("Description", newThemeDescription, sizeof(newThemeDescription), ImVec2(400 * scale, 80 * scale)); + ImGui::InputTextMultiline(T("menu.settings.description", "Description"), newThemeDescription, sizeof(newThemeDescription), ImVec2(400 * scale, 80 * scale)); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Optional description for the theme"); + ImGui::Text("%s", T("menu.settings.description_tooltip", "Optional description for the theme")); } ImGui::Separator(); // Buttons - if (Util::ButtonWithFlash("Create Theme")) { + if (Util::ButtonWithFlash(T("menu.settings.create_theme", "Create Theme"))) { if (!isThemeNameEmpty && !isDuplicateName && !isDuplicateDisplayName) { // Valid theme name, reset error state and proceed showValidationError = false; @@ -830,7 +889,7 @@ void SettingsTabRenderer::RenderThemesTab() } ImGui::SameLine(); - if (ImGui::Button("Cancel")) { + if (ImGui::Button(T("menu.settings.cancel", "Cancel"))) { showCreateThemePopup = false; ImGui::CloseCurrentPopup(); } @@ -853,15 +912,16 @@ void SettingsTabRenderer::RenderThemesTab() void SettingsTabRenderer::RenderFontsTab() { - if (BeginTabItemWithFont("Fonts", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_fonts", "Fonts"), "InterfaceFontsTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto* menuInstance = globals::menu; auto& themeSettings = menuInstance->GetSettings().Theme; RenderSaveInfoText(); - SeparatorTextWithFont("Font", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.font", "Font"), Menu::FontRole::Subheading); bool& useAutoFont = menuInstance->GetSettings().UseResolutionFont; - if (ImGui::Checkbox("Use resolution-based font size", &useAutoFont)) { + if (ImGui::Checkbox(T("menu.settings.use_resolution_based_font_size", "Use resolution-based font size"), &useAutoFont)) { if (!useAutoFont) { // Seed the fixed-size slider with the current effective size so it doesn't jump float effective = ThemeManager::ResolveFontSize(*menuInstance); @@ -870,17 +930,17 @@ void SettingsTabRenderer::RenderFontsTab() menuInstance->pendingFontReload = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("When enabled, the UI font size scales with your screen resolution. Disable to set a fixed size."); + ImGui::TextUnformatted(T("menu.settings.use_resolution_based_font_size_tooltip", "When enabled, the UI font size scales with your screen resolution. Disable to set a fixed size.")); } ImGui::BeginDisabled(useAutoFont); - if (ImGui::SliderFloat("Base Font Size", &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { + if (ImGui::SliderFloat(T("menu.settings.base_font_size", "Base Font Size"), &themeSettings.FontSize, ThemeManager::Constants::MIN_FONT_SIZE, ThemeManager::Constants::MAX_FONT_SIZE, "%.0f")) { menuInstance->pendingFontReload = true; } ImGui::EndDisabled(); float effectiveNow = ThemeManager::ResolveFontSize(*menuInstance); - ImGui::Text("Effective size: %.0f px", std::round(effectiveNow)); + ImGui::Text(T("menu.settings.effective_size", "Effective size: %.0f px"), std::round(effectiveNow)); static Util::Fonts::Catalog fontCatalog; static bool catalogInitialized = false; @@ -894,10 +954,10 @@ void SettingsTabRenderer::RenderFontsTab() } ImGui::Spacing(); - SeparatorTextWithFont("Font Roles", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.font_roles", "Font Roles"), Menu::FontRole::Subheading); if (fontCatalog.families.empty()) { - ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "No fonts found. Place .ttf files in Interface/CommunityShaders/Fonts/"); + ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "%s", T("menu.settings.no_fonts_found", "No fonts found. Place .ttf files in Interface/CommunityShaders/Fonts/")); } for (size_t roleIndex = 0; roleIndex < Menu::FontRoleDescriptors.size(); ++roleIndex) { @@ -924,13 +984,13 @@ void SettingsTabRenderer::RenderFontsTab() } } - const char* familyPreview = fontCatalog.families.empty() ? "No families" : fontCatalog.families[familyIndex].displayName.c_str(); + const char* familyPreview = fontCatalog.families.empty() ? T("menu.settings.no_families", "No families") : fontCatalog.families[familyIndex].displayName.c_str(); std::string familyLabel = std::format("{} Family##{}", descriptor.displayName, roleIndex); { FontRoleGuard familyComboFont(Menu::FontRole::Body); if (ImGui::BeginCombo(familyLabel.c_str(), familyPreview)) { if (fontCatalog.families.empty()) { - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "No font families available"); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", T("menu.settings.no_font_families_available", "No font families available")); } else { for (int i = 0; i < static_cast(fontCatalog.families.size()); ++i) { bool isSelected = (i == familyIndex); @@ -964,7 +1024,7 @@ void SettingsTabRenderer::RenderFontsTab() const Util::Fonts::FamilyInfo* selectedFamily = (fontCatalog.families.empty()) ? nullptr : &fontCatalog.families[familyIndex]; if (selectedFamily && selectedFamily->styles.empty()) { - ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "No style variants found for this family."); + ImGui::TextColored(ImVec4(0.9f, 0.6f, 0.2f, 1.0f), "%s", T("menu.settings.no_style_variants", "No style variants found for this family.")); } else if (selectedFamily) { int styleIndex = 0; for (size_t s = 0; s < selectedFamily->styles.size(); ++s) { @@ -976,7 +1036,7 @@ void SettingsTabRenderer::RenderFontsTab() if (styleIndex >= static_cast(selectedFamily->styles.size())) { styleIndex = 0; } - const char* stylePreview = selectedFamily->styles.empty() ? "No styles" : selectedFamily->styles[styleIndex].displayName.c_str(); + const char* stylePreview = selectedFamily->styles.empty() ? T("menu.settings.no_styles", "No styles") : selectedFamily->styles[styleIndex].displayName.c_str(); std::string styleLabel = std::format("{} Style##{}", descriptor.displayName, roleIndex); { FontRoleGuard styleComboFont(Menu::FontRole::Body); @@ -1004,7 +1064,7 @@ void SettingsTabRenderer::RenderFontsTab() } } - ImGui::TextDisabled("File: %s", roleSettings.File.c_str()); + ImGui::TextDisabled(T("menu.settings.file_label", "File: %s"), roleSettings.File.c_str()); std::string scaleLabel = std::format("{} Scale##{}", descriptor.displayName, roleIndex); if (ImGui::SliderFloat(scaleLabel.c_str(), &roleSettings.SizeScale, 0.5f, 2.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) { @@ -1019,13 +1079,16 @@ void SettingsTabRenderer::RenderFontsTab() // Add Feature Title Scale slider under Title font role if (role == Menu::FontRole::Title) { - ImGui::SliderFloat("Feature Header Scale", &themeSettings.FeatureHeading.FeatureTitleScale, 1.0f, 3.0f, "%.1fx", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T("menu.settings.feature_header_scale", "Feature Header Scale"), &themeSettings.FeatureHeading.FeatureTitleScale, 1.0f, 3.0f, "%.1fx", ImGuiSliderFlags_AlwaysClamp); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Scale multiplier for feature title text in the Settings tab."); + ImGui::Text("%s", T("menu.settings.feature_header_scale_tooltip", "Scale multiplier for feature title text in the Settings tab.")); } ImGui::SameLine(); - if (ImGui::Button("Reset##FeatureHeaderScale")) { - themeSettings.FeatureHeading.FeatureTitleScale = ThemeManager::Constants::DEFAULT_FEATURE_TITLE_SCALE; + { + std::string resetBtnLabel = std::string(T("menu.settings.reset", "Reset")) + "##FeatureHeaderScale"; + if (ImGui::Button(resetBtnLabel.c_str())) { + themeSettings.FeatureHeading.FeatureTitleScale = ThemeManager::Constants::DEFAULT_FEATURE_TITLE_SCALE; + } } } @@ -1033,11 +1096,11 @@ void SettingsTabRenderer::RenderFontsTab() ImGui::PopID(); } - if (ImGui::Button("Refresh Font Families")) { + if (ImGui::Button(T("menu.settings.refresh_font_families", "Refresh Font Families"))) { refreshFontCatalog(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Rescan the Fonts directory after adding or removing font files."); + ImGui::TextUnformatted(T("menu.settings.refresh_font_families_tooltip", "Rescan the Fonts directory after adding or removing font files.")); } ImGui::EndTabItem(); @@ -1046,81 +1109,89 @@ void SettingsTabRenderer::RenderFontsTab() void SettingsTabRenderer::RenderStylingTab() { - if (BeginTabItemWithFont("Styling", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_styling", "Styling"), "InterfaceStylingTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; auto& style = themeSettings.Style; RenderSaveInfoText(); - SeparatorTextWithFont("Main", Menu::FontRole::Subheading); - if (ImGui::SliderFloat("Global Scale", &themeSettings.GlobalScale, -1.f, 1.f, "%.2f")) { + SeparatorTextWithFont(T("menu.settings.section_main", "Main"), Menu::FontRole::Subheading); + if (ImGui::SliderFloat(T("menu.settings.global_scale", "Global Scale"), &themeSettings.GlobalScale, -1.f, 1.f, "%.2f")) { float trueScale = exp2(themeSettings.GlobalScale); ImGui::GetStyle().FontScaleMain = trueScale; } - SeparatorTextWithFont("Layout", Menu::FontRole::Subheading); + SeparatorTextWithFont(T("menu.settings.section_layout", "Layout"), Menu::FontRole::Subheading); - ImGui::SliderFloat2("Window Padding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("Frame Padding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("Item Spacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat2("Item Inner Spacing", (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); - ImGui::SliderFloat("Indent Spacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); - ImGui::SliderFloat("Scrollbar Size", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); - ImGui::SliderFloat("Grab Min Size", &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2(T("menu.settings.window_padding", "Window Padding"), (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2(T("menu.settings.frame_padding", "Frame Padding"), (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2(T("menu.settings.item_spacing", "Item Spacing"), (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2(T("menu.settings.item_inner_spacing", "Item Inner Spacing"), (float*)&style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.indent_spacing", "Indent Spacing"), &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.scrollbar_size", "Scrollbar Size"), &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.grab_min_size", "Grab Min Size"), &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); - SeparatorTextWithFont("Scrollbar Opacity", Menu::FontRole::Subheading); - ImGui::SliderFloat("Track Opacity", &themeSettings.ScrollbarOpacity.Background, 0.0f, 1.0f, "%.2f"); + SeparatorTextWithFont(T("menu.settings.scrollbar_opacity", "Scrollbar Opacity"), Menu::FontRole::Subheading); + ImGui::SliderFloat(T("menu.settings.track_opacity", "Track Opacity"), &themeSettings.ScrollbarOpacity.Background, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar)."); - ImGui::SliderFloat("Thumb Opacity", &themeSettings.ScrollbarOpacity.Thumb, 0.0f, 1.0f, "%.2f"); + ImGui::Text("%s", T("menu.settings.track_opacity_tooltip", "Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar).")); + ImGui::SliderFloat(T("menu.settings.thumb_opacity", "Thumb Opacity"), &themeSettings.ScrollbarOpacity.Thumb, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Controls the opacity of the scrollbar thumb (the draggable part)."); - ImGui::SliderFloat("Thumb Hovered Opacity", &themeSettings.ScrollbarOpacity.ThumbHovered, 0.0f, 1.0f, "%.2f"); + ImGui::Text("%s", T("menu.settings.thumb_opacity_tooltip", "Controls the opacity of the scrollbar thumb (the draggable part).")); + ImGui::SliderFloat(T("menu.settings.thumb_hovered_opacity", "Thumb Hovered Opacity"), &themeSettings.ScrollbarOpacity.ThumbHovered, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Controls the opacity of the scrollbar thumb when hovered."); - ImGui::SliderFloat("Thumb Active Opacity", &themeSettings.ScrollbarOpacity.ThumbActive, 0.0f, 1.0f, "%.2f"); + ImGui::Text("%s", T("menu.settings.thumb_hovered_opacity_tooltip", "Controls the opacity of the scrollbar thumb when hovered.")); + ImGui::SliderFloat(T("menu.settings.thumb_active_opacity", "Thumb Active Opacity"), &themeSettings.ScrollbarOpacity.ThumbActive, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Controls the opacity of the scrollbar thumb when being dragged."); - - SeparatorTextWithFont("Borders", Menu::FontRole::Subheading); - ImGui::SliderFloat("Window Border Size", &style.WindowBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SliderFloat("Child Border Size", &style.ChildBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SliderFloat("Popup Border Size", &style.PopupBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SliderFloat("Frame Border Size", &style.FrameBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SliderFloat("Tab Border Size", &style.TabBorderSize, 0.0f, 5.0f, "%.0f"); - ImGui::SliderFloat("Tab Bar Border Size", &style.TabBarBorderSize, 0.0f, 5.0f, "%.0f"); - - SeparatorTextWithFont("Rounding", Menu::FontRole::Subheading); - ImGui::SliderFloat("Window Rounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Child Rounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Frame Rounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Popup Rounding", &style.PopupRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Scrollbar Rounding", &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Grab Rounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f"); - ImGui::SliderFloat("Tab Rounding", &style.TabRounding, 0.0f, 12.0f, "%.0f"); - - SeparatorTextWithFont("Tables", Menu::FontRole::Subheading); - ImGui::SliderFloat2("Cell Padding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); - ImGui::SliderAngle("Table Angled Headers Angle", &style.TableAngledHeadersAngle, -50.0f, +50.0f); - - SeparatorTextWithFont("Widgets", Menu::FontRole::Subheading); + ImGui::Text("%s", T("menu.settings.thumb_active_opacity_tooltip", "Controls the opacity of the scrollbar thumb when being dragged.")); + + SeparatorTextWithFont(T("menu.settings.section_borders", "Borders"), Menu::FontRole::Subheading); + ImGui::SliderFloat(T("menu.settings.window_border_size", "Window Border Size"), &style.WindowBorderSize, 0.0f, 5.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.child_border_size", "Child Border Size"), &style.ChildBorderSize, 0.0f, 5.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.popup_border_size", "Popup Border Size"), &style.PopupBorderSize, 0.0f, 5.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.frame_border_size", "Frame Border Size"), &style.FrameBorderSize, 0.0f, 5.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.tab_border_size", "Tab Border Size"), &style.TabBorderSize, 0.0f, 5.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.tab_bar_border_size", "Tab Bar Border Size"), &style.TabBarBorderSize, 0.0f, 5.0f, "%.0f"); + + SeparatorTextWithFont(T("menu.settings.section_rounding", "Rounding"), Menu::FontRole::Subheading); + ImGui::SliderFloat(T("menu.settings.window_rounding", "Window Rounding"), &style.WindowRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.child_rounding", "Child Rounding"), &style.ChildRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.frame_rounding", "Frame Rounding"), &style.FrameRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.popup_rounding", "Popup Rounding"), &style.PopupRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.scrollbar_rounding", "Scrollbar Rounding"), &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.grab_rounding", "Grab Rounding"), &style.GrabRounding, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.tab_rounding", "Tab Rounding"), &style.TabRounding, 0.0f, 12.0f, "%.0f"); + + SeparatorTextWithFont(T("menu.settings.section_tables", "Tables"), Menu::FontRole::Subheading); + ImGui::SliderFloat2(T("menu.settings.cell_padding", "Cell Padding"), (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderAngle(T("menu.settings.table_angled_headers_angle", "Table Angled Headers Angle"), &style.TableAngledHeadersAngle, -50.0f, +50.0f); + + SeparatorTextWithFont(T("menu.settings.section_widgets", "Widgets"), Menu::FontRole::Subheading); { FontRoleGuard comboFont(Menu::FontRole::Body); - ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0"); + const char* colorButtonPositions[] = { + T("menu.settings.color_button_left", "Left"), + T("menu.settings.color_button_right", "Right") + }; + int colorButtonPos = (int)style.ColorButtonPosition; + if (ImGui::Combo(T("menu.settings.color_button_position", "ColorButtonPosition"), &colorButtonPos, colorButtonPositions, IM_ARRAYSIZE(colorButtonPositions))) { + style.ColorButtonPosition = static_cast(colorButtonPos); + } } - ImGui::SliderFloat2("Button Text Align", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat2(T("menu.settings.button_text_align", "Button Text Align"), (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Alignment applies when a button is larger than its text content."); - ImGui::SliderFloat2("Selectable Text Align", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::Text("%s", T("menu.settings.button_text_align_tooltip", "Alignment applies when a button is larger than its text content.")); + ImGui::SliderFloat2(T("menu.settings.selectable_text_align", "Selectable Text Align"), (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("Alignment applies when a selectable is larger than its text content."); - ImGui::SliderFloat("Separator Text Border Size", &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f"); - ImGui::SliderFloat2("Separator Text Align", (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui::SliderFloat2("Separator Text Padding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f"); - ImGui::SliderFloat("Log Slider Deadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui::Text("%s", T("menu.settings.selectable_text_align_tooltip", "Alignment applies when a selectable is larger than its text content.")); + ImGui::SliderFloat(T("menu.settings.separator_text_border_size", "Separator Text Border Size"), &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f"); + ImGui::SliderFloat2(T("menu.settings.separator_text_align", "Separator Text Align"), (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat2(T("menu.settings.separator_text_padding", "Separator Text Padding"), (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f"); + ImGui::SliderFloat(T("menu.settings.log_slider_deadzone", "Log Slider Deadzone"), &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); - SeparatorTextWithFont("Docking", Menu::FontRole::Subheading); - ImGui::SliderFloat("Docking Splitter Size", &style.DockingSeparatorSize, 0.0f, 12.0f, "%.0f"); + SeparatorTextWithFont(T("menu.settings.section_docking", "Docking"), Menu::FontRole::Subheading); + ImGui::SliderFloat(T("menu.settings.docking_splitter_size", "Docking Splitter Size"), &style.DockingSeparatorSize, 0.0f, 12.0f, "%.0f"); ImGui::EndTabItem(); } @@ -1128,7 +1199,8 @@ void SettingsTabRenderer::RenderStylingTab() void SettingsTabRenderer::RenderColorsTab() { - if (BeginTabItemWithFont("Colors", Menu::FontRole::Heading)) { + auto tabLabel = std::format("{}##{}", T("menu.settings.tab_colors", "Colors"), "InterfaceColorsTab"); + if (BeginTabItemWithFont(tabLabel.c_str(), Menu::FontRole::Heading)) { auto& themeSettings = globals::menu->GetSettings().Theme; auto& colors = themeSettings.FullPalette; RenderSaveInfoText(); @@ -1145,7 +1217,7 @@ void SettingsTabRenderer::RenderColorsTab() // Custom style for filter with icon space float scale = Util::GetUIScale(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f * scale)); - colorFilter.Draw("Filter colors", availableWidth); + colorFilter.Draw(T("menu.settings.filter_colors", "Filter colors"), availableWidth); ImGui::PopStyleVar(); // Draw search icon @@ -1168,52 +1240,52 @@ void SettingsTabRenderer::RenderColorsTab() // Background & Text if (colorFilter.PassFilter("Background")) - ImGui::ColorEdit4("Background", (float*)&themeSettings.Palette.Background); + ImGui::ColorEdit4(T("menu.settings.color_background", "Background"), (float*)&themeSettings.Palette.Background); if (colorFilter.PassFilter("Text")) - ImGui::ColorEdit4("Text", (float*)&themeSettings.Palette.Text); + ImGui::ColorEdit4(T("menu.settings.color_text", "Text"), (float*)&themeSettings.Palette.Text); - if (ImGui::TreeNodeEx("Borders & Separators", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNodeEx(T("menu.settings.borders_and_separators", "Borders & Separators"), ImGuiTreeNodeFlags_DefaultOpen)) { if (colorFilter.PassFilter("Window Border")) - ImGui::ColorEdit4("Window Border", (float*)&themeSettings.Palette.WindowBorder); + ImGui::ColorEdit4(T("menu.settings.color_window_border", "Window Border"), (float*)&themeSettings.Palette.WindowBorder); if (colorFilter.PassFilter("Slider & Input Background")) - ImGui::ColorEdit4("Slider & Input Background", (float*)&themeSettings.Palette.FrameBorder); + ImGui::ColorEdit4(T("menu.settings.color_slider_input_bg", "Slider & Input Background"), (float*)&themeSettings.Palette.FrameBorder); if (colorFilter.PassFilter("Separator Line")) - ImGui::ColorEdit4("Separator Line", (float*)&themeSettings.Palette.Separator); + ImGui::ColorEdit4(T("menu.settings.color_separator_line", "Separator Line"), (float*)&themeSettings.Palette.Separator); if (colorFilter.PassFilter("Resize Grip")) - ImGui::ColorEdit4("Resize Grip", (float*)&themeSettings.Palette.ResizeGrip); + ImGui::ColorEdit4(T("menu.settings.color_resize_grip", "Resize Grip"), (float*)&themeSettings.Palette.ResizeGrip); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Feature Headings", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNodeEx(T("menu.settings.feature_headings", "Feature Headings"), ImGuiTreeNodeFlags_DefaultOpen)) { if (colorFilter.PassFilter("Default")) - ImGui::ColorEdit4("Default", (float*)&themeSettings.FeatureHeading.ColorDefault); + ImGui::ColorEdit4(T("menu.settings.color_default", "Default"), (float*)&themeSettings.FeatureHeading.ColorDefault); if (colorFilter.PassFilter("Hovered")) - ImGui::ColorEdit4("Hovered", (float*)&themeSettings.FeatureHeading.ColorHovered); + ImGui::ColorEdit4(T("menu.settings.color_hovered", "Hovered"), (float*)&themeSettings.FeatureHeading.ColorHovered); if (colorFilter.PassFilter("Minimized Transparency")) - ImGui::SliderFloat("Minimized Transparency", &themeSettings.FeatureHeading.MinimizedFactor, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat(T("menu.settings.color_minimized_transparency", "Minimized Transparency"), &themeSettings.FeatureHeading.MinimizedFactor, 0.0f, 1.0f, "%.2f"); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Status", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::TreeNodeEx(T("menu.settings.status", "Status"), ImGuiTreeNodeFlags_DefaultOpen)) { if (colorFilter.PassFilter("Disabled")) - ImGui::ColorEdit4("Disabled", (float*)&themeSettings.StatusPalette.Disable); + ImGui::ColorEdit4(T("menu.settings.color_disabled", "Disabled"), (float*)&themeSettings.StatusPalette.Disable); if (colorFilter.PassFilter("Error")) - ImGui::ColorEdit4("Error", (float*)&themeSettings.StatusPalette.Error); + ImGui::ColorEdit4(T("menu.settings.color_error", "Error"), (float*)&themeSettings.StatusPalette.Error); if (colorFilter.PassFilter("Warning")) - ImGui::ColorEdit4("Warning", (float*)&themeSettings.StatusPalette.Warning); + ImGui::ColorEdit4(T("menu.settings.color_warning", "Warning"), (float*)&themeSettings.StatusPalette.Warning); if (colorFilter.PassFilter("Restart Needed")) - ImGui::ColorEdit4("Restart Needed", (float*)&themeSettings.StatusPalette.RestartNeeded); + ImGui::ColorEdit4(T("menu.settings.color_restart_needed", "Restart Needed"), (float*)&themeSettings.StatusPalette.RestartNeeded); if (colorFilter.PassFilter("Current Hotkey")) - ImGui::ColorEdit4("Current Hotkey", (float*)&themeSettings.StatusPalette.CurrentHotkey); + ImGui::ColorEdit4(T("menu.settings.color_current_hotkey", "Current Hotkey"), (float*)&themeSettings.StatusPalette.CurrentHotkey); if (colorFilter.PassFilter("Success")) - ImGui::ColorEdit4("Success", (float*)&themeSettings.StatusPalette.SuccessColor); + ImGui::ColorEdit4(T("menu.settings.color_success", "Success"), (float*)&themeSettings.StatusPalette.SuccessColor); if (colorFilter.PassFilter("Info")) - ImGui::ColorEdit4("Info", (float*)&themeSettings.StatusPalette.InfoColor); + ImGui::ColorEdit4(T("menu.settings.color_info", "Info"), (float*)&themeSettings.StatusPalette.InfoColor); ImGui::TreePop(); } - if (ImGui::TreeNode("Full Palette")) { - ImGui::TextWrapped("Advanced color controls for detailed customization of all UI elements."); + if (ImGui::TreeNode(T("menu.settings.full_palette", "Full Palette"))) { + ImGui::TextWrapped("%s", T("menu.settings.full_palette_tooltip", "Advanced color controls for detailed customization of all UI elements.")); for (int i = 0; i < ImGuiCol_COUNT; i++) { const char* friendlyName = GetFriendlyColorName(i); diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 31da7155e5..94b8533c1f 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -4,10 +4,12 @@ #include "BackgroundBlur.h" #include "Fonts.h" +#include "I18n/I18n.h" #include #include #include +#include #include #include #include @@ -59,6 +61,94 @@ namespace return 0; } } + + bool IsSimplifiedChineseLocale(const std::string& locale) + { + return locale == "zh" || + locale == "zh_CN" || + locale == "zh_SG" || + locale == "zh_Hans" || + locale.starts_with("zh-Hans"); + } + + bool IsTraditionalChineseLocale(const std::string& locale) + { + return locale == "zh_TW" || + locale == "zh_HK" || + locale == "zh_MO" || + locale == "zh_Hant" || + locale.starts_with("zh-Hant"); + } + + std::vector GetCJKFontPathCandidates(const std::string& locale) + { + std::vector candidates; + + auto addCandidate = [&](std::filesystem::path path) { + auto candidate = path.string(); + if (!candidate.empty() && std::find(candidates.begin(), candidates.end(), candidate) == candidates.end()) { + candidates.push_back(std::move(candidate)); + } + }; + + std::filesystem::path windowsFonts = "C:\\Windows\\Fonts"; + if (const char* windir = std::getenv("WINDIR"); windir && windir[0] != '\0') { + windowsFonts = std::filesystem::path(windir) / "Fonts"; + } + + if (locale.starts_with("zh")) { + if (IsTraditionalChineseLocale(locale)) { + addCandidate(windowsFonts / "msjh.ttc"); + addCandidate(windowsFonts / "mingliu.ttc"); + } else { + addCandidate(windowsFonts / "msyh.ttc"); + addCandidate(windowsFonts / "simsun.ttc"); + addCandidate(windowsFonts / "simhei.ttf"); + } + } else if (locale == "ja") { + addCandidate(windowsFonts / "meiryo.ttc"); + addCandidate(windowsFonts / "msgothic.ttc"); + } else if (locale == "ko") { + addCandidate(windowsFonts / "malgun.ttf"); + addCandidate(windowsFonts / "gulim.ttc"); + } + + return candidates; + } + + std::string FormatFontCandidateStatus(const std::vector& candidates) + { + std::string result; + for (const auto& candidate : candidates) { + if (!result.empty()) { + result += "; "; + } + result += std::format("{} exists={}", candidate, std::filesystem::exists(candidate) ? "yes" : "no"); + } + return result; + } + + bool ContainsNonAscii(std::string_view text) + { + return std::ranges::any_of(text, [](unsigned char ch) { return ch >= 0x80; }); + } + + const ImWchar* GetPrimaryCJKGlyphRanges(ImFontAtlas* atlas, const std::string& locale) + { + if (locale.starts_with("zh")) { + if (IsTraditionalChineseLocale(locale)) { + return atlas->GetGlyphRangesChineseFull(); + } + return atlas->GetGlyphRangesChineseSimplifiedCommon(); + } + if (locale == "ja") { + return atlas->GetGlyphRangesJapanese(); + } + if (locale == "ko") { + return atlas->GetGlyphRangesKorean(); + } + return nullptr; + } } // Static UI helper methods @@ -360,6 +450,135 @@ bool ThemeManager::ReloadFont(const Menu& menu, float& cachedFontSize) const_cast(menu).GetSettings().Theme.FontName = menu.cachedFontName; const_cast(menu).cachedFontSignature = const_cast(menu).BuildFontSignature(fontSize); + // ─── CJK Font Merging ──────────────────────────────────────────────────────── + // Merge glyphs needed by the active locale, plus the minimum glyph set needed + // to render available locale names in the language picker. + { + auto* i18n = I18n::GetSingleton(); + auto locale = i18n->GetCurrentLocale(); + const ImWchar* primaryGlyphRanges = GetPrimaryCJKGlyphRanges(io.Fonts, locale); + auto primaryCJKFontPaths = primaryGlyphRanges ? GetCJKFontPathCandidates(locale) : std::vector{}; + + struct SupplementalGlyphMerge + { + std::string locale; + std::vector fontPaths; + ImVector glyphRanges; + }; + + std::vector supplementalGlyphMerges; + for (const auto& [availableLocale, displayName] : i18n->GetAvailableLocales()) { + if (!ContainsNonAscii(displayName)) { + continue; + } + if (availableLocale == locale && primaryGlyphRanges) { + continue; + } + + auto fontPaths = GetCJKFontPathCandidates(availableLocale); + if (fontPaths.empty()) { + logger::warn("[I18n] No supplemental CJK font path candidates for locale display '{}': {}", availableLocale, displayName); + continue; + } + + ImFontGlyphRangesBuilder builder; + builder.AddText(displayName.c_str()); + + SupplementalGlyphMerge merge{ .locale = availableLocale, .fontPaths = std::move(fontPaths) }; + builder.BuildRanges(&merge.glyphRanges); + if (merge.glyphRanges.Size > 0) { + supplementalGlyphMerges.push_back(std::move(merge)); + } + } + + if (primaryGlyphRanges || !supplementalGlyphMerges.empty()) { + if (primaryGlyphRanges && primaryCJKFontPaths.empty()) { + logger::warn("[I18n] CJK locale '{}' active but no CJK font path candidates were available.", locale); + } else { + io.Fonts->Clear(); + + std::unordered_map cjkAtlasCache; + bool mergedAnyCJKFont = false; + + auto tryMergeGlyphSet = [&](ImFont* baseFont, + float roleSize, + const std::vector& fontPaths, + const ImWchar* glyphRanges, + const std::string& description, + Menu::FontRole role) { + if (!glyphRanges || fontPaths.empty()) { + return; + } + + ImFontConfig mergeCfg; + mergeCfg.MergeMode = true; + mergeCfg.DstFont = baseFont; + mergeCfg.OversampleH = Constants::FCONF_OVERSAMPLE_H; + mergeCfg.OversampleV = Constants::FCONF_OVERSAMPLE_V; + mergeCfg.PixelSnapH = Constants::FCONF_PIXELSNAP_H; + + for (const auto& cjkFontPath : fontPaths) { + if (io.Fonts->AddFontFromFileTTF(cjkFontPath.c_str(), roleSize, &mergeCfg, glyphRanges)) { + mergedAnyCJKFont = true; + return; + } + } + + logger::warn("[I18n] Failed to merge {} for role '{}'. Tried: {}", + description, + Menu::GetFontRoleKey(role), + FormatFontCandidateStatus(fontPaths)); + }; + + for (size_t i = 0; i < static_cast(Menu::FontRole::Count); ++i) { + float roleSize = menu.cachedFontPixelSizesByRole[i]; + std::string roleFile = const_cast(menu).cachedFontFilesByRole[i]; + Menu::FontRole role = static_cast(i); + + if (roleFile.empty()) { + roleFile = Menu::GetDefaultFontRole(role).File; + } + + auto fontPath = fontsRoot / roleFile; + std::string cacheKey = std::format("{}|{}", roleFile, static_cast(roleSize)); + + auto cached = cjkAtlasCache.find(cacheKey); + if (cached != cjkAtlasCache.end()) { + menu.loadedFontRoles[i] = cached->second; + continue; + } + + ImFontConfig baseCfg = font_config; + ImFont* baseFont = nullptr; + if (std::filesystem::exists(fontPath)) { + baseFont = io.Fonts->AddFontFromFileTTF(fontPath.string().c_str(), roleSize, &baseCfg); + } + + if (!baseFont) { + baseFont = io.Fonts->AddFontDefault(); + } + + tryMergeGlyphSet(baseFont, roleSize, primaryCJKFontPaths, primaryGlyphRanges, std::format("active locale '{}' glyphs", locale), role); + for (const auto& merge : supplementalGlyphMerges) { + tryMergeGlyphSet(baseFont, roleSize, merge.fontPaths, merge.glyphRanges.Data, std::format("locale display '{}'", merge.locale), role); + } + + menu.loadedFontRoles[i] = baseFont; + cjkAtlasCache.emplace(cacheKey, baseFont); + } + + bodyFont = menu.loadedFontRoles[static_cast(Menu::FontRole::Body)]; + io.FontDefault = bodyFont; + + if (mergedAnyCJKFont) { + logger::info("[I18n] Rebuilt font atlas with locale glyph support for '{}'", locale); + } else { + logger::warn("[I18n] Rebuilt font atlas without supplemental locale glyphs for '{}'", locale); + } + } + } + } + // Build the font atlas - this bakes all fonts into the texture if (!io.Fonts->Build()) { logger::error("ReloadFont: Failed to build font atlas"); diff --git a/src/State.cpp b/src/State.cpp index 1e4bca201c..9bc07e59fd 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -6,6 +6,7 @@ #include "Deferred.h" #include "FeatureIssues.h" +#include "Features/CSEditor.h" #include "Features/CloudShadows.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" @@ -16,7 +17,6 @@ #include "Features/Upscaling.h" #include "Features/VRStereoOptimizations.h" #include "Features/VolumetricShadows.h" -#include "Features/CSEditor.h" #include "Menu.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" @@ -454,6 +454,7 @@ void State::SaveToJson(nlohmann::json& settings) general["Enable Disk Cache"] = shaderCache->IsDiskCache(); general["Skip Unchanged Shaders"] = shaderCache->IsSkipUnchangedShaders(); general["Enable Async"] = shaderCache->IsAsync(); + general["Language"] = I18n::GetSingleton()->GetCurrentLocale(); settings["General"] = general; @@ -535,6 +536,23 @@ void State::LoadFromJson(nlohmann::json& settings) shaderCache->SetSkipUnchangedShaders(general["Skip Unchanged Shaders"]); if (general.contains("Enable Async") && general["Enable Async"].is_boolean()) shaderCache->SetAsync(general["Enable Async"]); + + // Load i18n locale preference + if (general.contains("Language") && general["Language"].is_string()) { + auto locale = general["Language"].get(); + auto* i18n = I18n::GetSingleton(); + if (locale != i18n->GetCurrentLocale()) { + i18n->SetLocale(locale); + } + } else { + // No saved language preference — auto-detect from system locale on first launch + auto* i18n = I18n::GetSingleton(); + auto detected = i18n->DetectSystemLocale(); + if (detected != "en" && detected != i18n->GetCurrentLocale()) { + i18n->SetLocale(detected); + logger::info("[I18n] Auto-detected system locale: '{}'", detected); + } + } } if (settings.contains("Replace Original Shaders") && settings["Replace Original Shaders"].is_object()) { diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 279fc86cd6..4dc47668d9 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -5,10 +5,13 @@ #include "Features/InteriorSun.h" #include "Hooks.h" +#include "I18n/I18n.h" #include "ShaderCache.h" #include "State.h" #include "Util.h" +#define I18N_KEY_PREFIX "feature.true_pbr." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( GlintParameters, enabled, @@ -110,70 +113,70 @@ void SetupPBRLandscapeTextureParameters(BSLightingShaderMaterialPBRLandscape& ma void TruePBR::DrawSettings() { - if (ImGui::TreeNodeEx("Global Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Vertex AO Strength", &settings.VertexAOStrength, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (ImGui::TreeNodeEx(T(TKEY("global_settings"), "Global Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat(T(TKEY("vertex_ao_strength"), "Vertex AO Strength"), &settings.VertexAOStrength, 0.f, 1.f, "%.2f", ImGuiSliderFlags_AlwaysClamp); ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Texture Set Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (Util::SearchableCombo("Texture Set", selectedPbrTextureSetName, pbrTextureSets)) { + if (ImGui::TreeNodeEx(T(TKEY("texture_set_settings"), "Texture Set Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (Util::SearchableCombo(T(TKEY("texture_set"), "Texture Set"), selectedPbrTextureSetName, pbrTextureSets)) { selectedPbrTextureSet = &pbrTextureSets[selectedPbrTextureSetName]; } if (selectedPbrTextureSet != nullptr) { bool wasEdited = false; - if (ImGui::SliderFloat("Displacement Scale", &selectedPbrTextureSet->displacementScale, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("displacement_scale"), "Displacement Scale"), &selectedPbrTextureSet->displacementScale, 0.f, 3.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Roughness Scale", &selectedPbrTextureSet->roughnessScale, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("roughness_scale"), "Roughness Scale"), &selectedPbrTextureSet->roughnessScale, 0.f, 3.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Specular Level", &selectedPbrTextureSet->specularLevel, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("specular_level"), "Specular Level"), &selectedPbrTextureSet->specularLevel, 0.f, 3.f, "%.3f")) { wasEdited = true; } - if (ImGui::TreeNodeEx("Subsurface")) { - if (ImGui::ColorPicker3("Subsurface Color", &selectedPbrTextureSet->subsurfaceColor.red)) { + if (ImGui::TreeNodeEx(T(TKEY("subsurface"), "Subsurface"))) { + if (ImGui::ColorPicker3(T(TKEY("subsurface_color"), "Subsurface Color"), &selectedPbrTextureSet->subsurfaceColor.red)) { wasEdited = true; } - if (ImGui::SliderFloat("Subsurface Opacity", &selectedPbrTextureSet->subsurfaceOpacity, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("subsurface_opacity"), "Subsurface Opacity"), &selectedPbrTextureSet->subsurfaceOpacity, 0.f, 1.f, "%.3f")) { wasEdited = true; } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Coat")) { - if (ImGui::ColorPicker3("Coat Color", &selectedPbrTextureSet->coatColor.red)) { + if (ImGui::TreeNodeEx(T(TKEY("coat"), "Coat"))) { + if (ImGui::ColorPicker3(T(TKEY("coat_color"), "Coat Color"), &selectedPbrTextureSet->coatColor.red)) { wasEdited = true; } - if (ImGui::SliderFloat("Coat Strength", &selectedPbrTextureSet->coatStrength, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("coat_strength"), "Coat Strength"), &selectedPbrTextureSet->coatStrength, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Coat Roughness", &selectedPbrTextureSet->coatRoughness, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("coat_roughness"), "Coat Roughness"), &selectedPbrTextureSet->coatRoughness, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Coat Specular Level", &selectedPbrTextureSet->coatSpecularLevel, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("coat_specular_level"), "Coat Specular Level"), &selectedPbrTextureSet->coatSpecularLevel, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Inner Layer Displacement Offset", &selectedPbrTextureSet->innerLayerDisplacementOffset, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("inner_layer_displacement_offset"), "Inner Layer Displacement Offset"), &selectedPbrTextureSet->innerLayerDisplacementOffset, 0.f, 3.f, "%.3f")) { wasEdited = true; } ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Glint")) { - if (ImGui::Checkbox("Enabled", &selectedPbrTextureSet->glintParameters.enabled)) { + if (ImGui::TreeNodeEx(T(TKEY("glint"), "Glint"))) { + if (ImGui::Checkbox(T(TKEY("enabled"), "Enabled"), &selectedPbrTextureSet->glintParameters.enabled)) { wasEdited = true; } if (selectedPbrTextureSet->glintParameters.enabled) { - if (ImGui::SliderFloat("Screenspace Scale", &selectedPbrTextureSet->glintParameters.screenSpaceScale, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("screenspace_scale"), "Screenspace Scale"), &selectedPbrTextureSet->glintParameters.screenSpaceScale, 0.f, 3.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Log Microfacet Density", &selectedPbrTextureSet->glintParameters.logMicrofacetDensity, 0.f, 40.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("log_microfacet_density"), "Log Microfacet Density"), &selectedPbrTextureSet->glintParameters.logMicrofacetDensity, 0.f, 40.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Microfacet Roughness", &selectedPbrTextureSet->glintParameters.microfacetRoughness, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("microfacet_roughness"), "Microfacet Roughness"), &selectedPbrTextureSet->glintParameters.microfacetRoughness, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Density Randomization", &selectedPbrTextureSet->glintParameters.densityRandomization, 0.f, 5.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("density_randomization"), "Density Randomization"), &selectedPbrTextureSet->glintParameters.densityRandomization, 0.f, 5.f, "%.3f")) { wasEdited = true; } } @@ -194,7 +197,7 @@ void TruePBR::DrawSettings() } } if (selectedPbrTextureSet != nullptr) { - if (ImGui::Button("Save")) { + if (ImGui::Button(T(TKEY("save"), "Save"))) { PNState::SavePBRRecordConfig("Data\\PBRTextureSets", selectedPbrTextureSetName, *selectedPbrTextureSet); } } @@ -202,15 +205,16 @@ void TruePBR::DrawSettings() ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Material Object Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (Util::SearchableCombo("Material Object", selectedPbrMaterialObjectName, pbrMaterialObjects)) { + if (ImGui::TreeNodeEx(T(TKEY("material_object_settings"), "Material Object Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { + if (Util::SearchableCombo(T(TKEY("material_object"), "Material Object"), selectedPbrMaterialObjectName, pbrMaterialObjects)) { selectedPbrMaterialObject = &pbrMaterialObjects[selectedPbrMaterialObjectName]; } if (selectedPbrMaterialObject != nullptr) { bool wasEdited = false; - if (ImGui::TreeNodeEx("Base Color Scale", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Button("Reset to 1.0##BaseColorScale")) { + if (ImGui::TreeNodeEx(T(TKEY("base_color_scale"), "Base Color Scale"), ImGuiTreeNodeFlags_DefaultOpen)) { + auto resetBaseColorScaleLabel = std::string(T(TKEY("reset_to_1_0"), "Reset to 1.0")) + "##BaseColorScale"; + if (ImGui::Button(resetBaseColorScaleLabel.c_str())) { selectedPbrMaterialObject->baseColorScale = { 1.f, 1.f, 1.f }; wasEdited = true; } @@ -225,7 +229,7 @@ void TruePBR::DrawSettings() ImGui::AlignTextToFramePadding(); ImGui::SetCursorPosX(colorLabelStartX); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); - ImGui::Text("Red"); + ImGui::TextUnformatted(T(TKEY("red"), "Red")); ImGui::PopStyleColor(); ImGui::SameLine(sliderStartX); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.4f, 0.1f, 0.1f, 0.6f)); @@ -239,7 +243,7 @@ void TruePBR::DrawSettings() ImGui::AlignTextToFramePadding(); ImGui::SetCursorPosX(colorLabelStartX); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); - ImGui::Text("Green"); + ImGui::TextUnformatted(T(TKEY("green"), "Green")); ImGui::PopStyleColor(); ImGui::SameLine(sliderStartX); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.4f, 0.1f, 0.6f)); @@ -253,7 +257,7 @@ void TruePBR::DrawSettings() ImGui::AlignTextToFramePadding(); ImGui::SetCursorPosX(colorLabelStartX); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.3f, 0.9f, 1.0f)); - ImGui::Text("Blue"); + ImGui::TextUnformatted(T(TKEY("blue"), "Blue")); ImGui::PopStyleColor(); ImGui::SameLine(sliderStartX); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.4f, 0.6f)); @@ -266,27 +270,27 @@ void TruePBR::DrawSettings() ImGui::TreePop(); } - if (ImGui::SliderFloat("Roughness", &selectedPbrMaterialObject->roughness, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("roughness"), "Roughness"), &selectedPbrMaterialObject->roughness, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Specular Level", &selectedPbrMaterialObject->specularLevel, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("material_specular_level"), "Specular Level"), &selectedPbrMaterialObject->specularLevel, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::TreeNodeEx("Glint")) { - if (ImGui::Checkbox("Enabled", &selectedPbrMaterialObject->glintParameters.enabled)) { + if (ImGui::TreeNodeEx(T(TKEY("material_glint"), "Glint"))) { + if (ImGui::Checkbox(T(TKEY("material_glint_enabled"), "Enabled"), &selectedPbrMaterialObject->glintParameters.enabled)) { wasEdited = true; } if (selectedPbrMaterialObject->glintParameters.enabled) { - if (ImGui::SliderFloat("Screenspace Scale", &selectedPbrMaterialObject->glintParameters.screenSpaceScale, 0.f, 3.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("material_screenspace_scale"), "Screenspace Scale"), &selectedPbrMaterialObject->glintParameters.screenSpaceScale, 0.f, 3.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Log Microfacet Density", &selectedPbrMaterialObject->glintParameters.logMicrofacetDensity, 0.f, 40.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("material_log_microfacet_density"), "Log Microfacet Density"), &selectedPbrMaterialObject->glintParameters.logMicrofacetDensity, 0.f, 40.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Microfacet Roughness", &selectedPbrMaterialObject->glintParameters.microfacetRoughness, 0.f, 1.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("material_microfacet_roughness"), "Microfacet Roughness"), &selectedPbrMaterialObject->glintParameters.microfacetRoughness, 0.f, 1.f, "%.3f")) { wasEdited = true; } - if (ImGui::SliderFloat("Density Randomization", &selectedPbrMaterialObject->glintParameters.densityRandomization, 0.f, 5.f, "%.3f")) { + if (ImGui::SliderFloat(T(TKEY("material_density_randomization"), "Density Randomization"), &selectedPbrMaterialObject->glintParameters.densityRandomization, 0.f, 5.f, "%.3f")) { wasEdited = true; } } @@ -300,7 +304,7 @@ void TruePBR::DrawSettings() } } if (selectedPbrMaterialObject != nullptr) { - if (ImGui::Button("Save")) { + if (ImGui::Button(T(TKEY("material_save"), "Save"))) { PNState::SavePBRRecordConfig("Data\\PBRMaterialObjects", selectedPbrMaterialObjectName, *selectedPbrMaterialObject); } } @@ -324,6 +328,8 @@ void TruePBR::RestoreDefaultSettings() settings = {}; } +#undef I18N_KEY_PREFIX + void TruePBR::SetupResources() { SetupTextureSetData(); diff --git a/src/TruePBR.h b/src/TruePBR.h index 5607efbfe6..044baea62b 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -15,6 +15,7 @@ struct TruePBR : Feature { public: virtual std::string GetName() override { return "True PBR"; } + virtual std::string GetDisplayName() override { return T("feature.true_pbr.name", "True PBR"); } virtual std::string GetShortName() override { return "TruePBR"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } virtual bool IsCore() const override { return true; } diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index 782dfda6dd..6fcbaef976 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -82,6 +82,11 @@ namespace Util return GetCommunityShaderPath() / "Themes"; } + std::filesystem::path GetTranslationsPath() + { + return GetCommunityShaderPath() / "Translations"; + } + std::filesystem::path GetOverridesPath() { return GetCommunityShaderPath() / "Overrides"; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index b4f8704811..ec1cfd194c 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -91,6 +91,12 @@ namespace Util */ std::filesystem::path GetThemesPath(); + /** + * Gets the Translations directory path for i18n locale files + * @return CommunityShaderPath / "Translations" + */ + std::filesystem::path GetTranslationsPath(); + /** * Gets the Overrides directory path * @return CommunityShaderPath / "Overrides" diff --git a/src/Utils/GameSetting.cpp b/src/Utils/GameSetting.cpp index 9b286871b6..0e28e62d65 100644 --- a/src/Utils/GameSetting.cpp +++ b/src/Utils/GameSetting.cpp @@ -1,4 +1,5 @@ #include "GameSetting.h" +#include "I18n/I18n.h" #include "Utils/UI.h" @@ -220,7 +221,7 @@ namespace Util } if (settingData.offset != 0) { ImGui::SameLine(); - if (ImGui::Button("Copy")) { + if (ImGui::Button(::T("ui.copy", "Copy"))) { ImGui::SetClipboardText(settingName.c_str()); } if (auto _tt = HoverTooltipWrapper()) { diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 2a9fb590ef..1c39b22e22 100644 --- a/src/Utils/Subrect.cpp +++ b/src/Utils/Subrect.cpp @@ -1,5 +1,6 @@ #include "Utils/Subrect.h" +#include "../I18n/I18n.h" #include #include diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 045b55147c..f23dd9b7e6 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,6 +1,7 @@ #include "UI.h" #include "../CSEditor/EditorWindow.h" +#include "../I18n/I18n.h" #include "D3D.h" #include "FileSystem.h" #include "Menu.h" @@ -280,21 +281,22 @@ namespace Util if (!showClearCacheConfirmation) return; - ImGui::OpenPopup("Clear Shader Cache?"); + ImGui::OpenPopup(T("ui.clear_shader_cache", "Clear Shader Cache?")); - if (auto popup = CenteredPopupModal("Clear Shader Cache?", &showClearCacheConfirmation)) { - ImGui::Text("Are you sure you want to clear the shader cache?"); + if (auto popup = CenteredPopupModal(T("ui.clear_shader_cache", "Clear Shader Cache?"), &showClearCacheConfirmation)) { + ImGui::Text("%s", T("ui.clear_cache_confirm", "Are you sure you want to clear the shader cache?")); ImGui::Spacing(); ImGui::Spacing(); ImGui::TextWrapped( - "This will clear all compiled shaders from memory and disk cache (if enabled). " - "Shaders will be recompiled when the game next encounters them."); + "%s", T("ui.clear_cache_desc", + "This will clear all compiled shaders from memory and disk cache (if enabled). " + "Shaders will be recompiled when the game next encounters them.")); ImGui::Spacing(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImGui::Checkbox("Don't ask me again", &dontAskAgainCheckbox); + ImGui::Checkbox(T("ui.dont_ask_again", "Don't ask me again"), &dontAskAgainCheckbox); ImGui::Spacing(); @@ -307,7 +309,7 @@ namespace Util if (offset > 0) ImGui::SetCursorPosX(offset); - if (ImGui::Button("Clear Cache", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T("ui.clear_cache", "Clear Cache"), ImVec2(buttonWidth, 0))) { // Save preference if checkbox is checked if (dontAskAgainCheckbox) { if (auto* menu = globals::menu) { @@ -322,7 +324,7 @@ namespace Util ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) { + if (ImGui::Button(T("ui.cancel", "Cancel"), ImVec2(buttonWidth, 0))) { showClearCacheConfirmation = false; ImGui::CloseCurrentPopup(); } @@ -361,7 +363,7 @@ namespace Util ImGui::Spacing(); if (showDontAskAgain) - ImGui::Checkbox("Don't ask me again", &dontAskCheckbox); + ImGui::Checkbox(T("ui.dont_ask_again", "Don't ask me again"), &dontAskCheckbox); constexpr float buttonWidth = ThemeManager::Constants::POPUP_BUTTON_WIDTH; const float spacing = ImGui::GetStyle().ItemSpacing.x; @@ -742,36 +744,36 @@ namespace Util return m_shouldDraw; } - bool DrawCategoryHeader(const char* categoryName, bool& isExpanded, int categoryCount) + bool DrawCategoryHeader(const char* categoryKey, const char* displayName, bool& isExpanded, int categoryCount) { // Get the appropriate icon for this category ID3D11ShaderResourceView* categoryIcon = nullptr; auto& menu = Menu::GetSingleton()->uiIcons; - if (strcmp(categoryName, "Characters") == 0) { + if (strcmp(categoryKey, "Characters") == 0) { categoryIcon = menu.characters.texture; - } else if (strcmp(categoryName, "Display") == 0) { + } else if (strcmp(categoryKey, "Display") == 0) { categoryIcon = menu.display.texture; - } else if (strcmp(categoryName, "Grass") == 0) { + } else if (strcmp(categoryKey, "Grass") == 0) { categoryIcon = menu.grass.texture; - } else if (strcmp(categoryName, "Lighting") == 0) { + } else if (strcmp(categoryKey, "Lighting") == 0) { categoryIcon = menu.lighting.texture; - } else if (strcmp(categoryName, "Sky") == 0) { + } else if (strcmp(categoryKey, "Sky") == 0) { categoryIcon = menu.sky.texture; - } else if (strcmp(categoryName, "Landscape & Textures") == 0) { + } else if (strcmp(categoryKey, "Landscape & Textures") == 0) { categoryIcon = menu.landscape.texture; - } else if (strcmp(categoryName, "Water") == 0) { + } else if (strcmp(categoryKey, "Water") == 0) { categoryIcon = menu.water.texture; - } else if (strcmp(categoryName, "Utility") == 0) { + } else if (strcmp(categoryKey, "Utility") == 0) { categoryIcon = menu.debug.texture; - } else if (strcmp(categoryName, "Materials") == 0) { + } else if (strcmp(categoryKey, "Materials") == 0) { categoryIcon = menu.materials.texture; - } else if (strcmp(categoryName, "Post-Processing") == 0) { + } else if (strcmp(categoryKey, "Post-Processing") == 0) { categoryIcon = menu.postProcessing.texture; } - // Add categoryCount to categoryName - std::string displayName = std::format("{} ({})", categoryName, categoryCount); + // Keep icon lookup on the stable category key and render the translated label separately. + std::string headerText = std::format("{} ({})", displayName, categoryCount); // Draw category header with custom styling ImDrawList* drawList = ImGui::GetWindowDrawList(); @@ -783,7 +785,7 @@ namespace Util const float currentFontSize = ImGui::GetFontSize(); const float iconSize = currentFontSize * 1.2f; // 20% larger than font height const float iconSpacing = currentFontSize * 0.3f; // 30% of font height for spacing - ImVec2 textSize = ImGui::CalcTextSize(displayName.c_str()); + ImVec2 textSize = ImGui::CalcTextSize(headerText.c_str()); // Calculate total content width (icon + spacing + text) float contentWidth = textSize.x; @@ -796,7 +798,7 @@ namespace Util float lineLength = (availableWidth - contentWidth - 20.0f) * 0.5f; // 20px for padding // Create selectable area for the entire header - ImGui::PushID(displayName.c_str()); + ImGui::PushID(categoryKey); bool hovered = false; bool clicked = false; @@ -850,7 +852,7 @@ namespace Util // Center text ImVec2 textPos = ImVec2(currentX, pos.y + 2.0f); - drawList->AddText(textPos, headerColor, displayName.c_str()); + drawList->AddText(textPos, headerColor, headerText.c_str()); // Handle click to toggle expansion if (clicked) { @@ -1233,7 +1235,7 @@ namespace Util snprintf(widgetId, sizeof(widgetId), "##%s_search", id); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(paddingLeft, ImGui::GetStyle().FramePadding.y)); - ImGui::InputTextWithHint(widgetId, "Search...", state.buffer, IM_ARRAYSIZE(state.buffer)); + ImGui::InputTextWithHint(widgetId, T("ui.search", "Search..."), state.buffer, IM_ARRAYSIZE(state.buffer)); ImGui::PopStyleVar(); ImVec2 iconPos = ImVec2( @@ -1288,7 +1290,7 @@ namespace Util strncpy_s(buffer, searchString.c_str(), sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; - if (ImGui::InputTextWithHint("##feature_search", "Search Features...", buffer, sizeof(buffer))) { + if (ImGui::InputTextWithHint("##feature_search", T("ui.search_features", "Search Features..."), buffer, sizeof(buffer))) { searchString = buffer; } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 7a7aec368a..5848aa470a 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -497,7 +497,7 @@ namespace Util * @param categoryCount Number of features in the category * @return true if the expansion state was toggled */ - bool DrawCategoryHeader(const char* categoryName, bool& isExpanded, int categoryCount); + bool DrawCategoryHeader(const char* categoryKey, const char* displayName, bool& isExpanded, int categoryCount); /** * Draws a custom styled section header with lines extending from both sides diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index f1d7f6ac98..a17f7e0751 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -3,6 +3,7 @@ #include "FrameAnnotations.h" #include "Globals.h" #include "Hooks.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Menu/ThemeManager.h" #include "SceneSettingsManager.h" @@ -161,6 +162,10 @@ bool Load() globals::ReInit(); auto state = globals::state; + + // Initialize i18n system (loads English fallback and discovers available locales) + I18n::GetSingleton()->Init(); + state->Load(); state->LoadTheme(); // Load theme settings from SettingsTheme.json diff --git a/tools/extract-i18n.py b/tools/extract-i18n.py new file mode 100644 index 0000000000..2d51239808 --- /dev/null +++ b/tools/extract-i18n.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +""" +extract-i18n.py — Extract translatable strings from Community Shaders source code. + +Scans all .cpp and .h files under src/ for T("key", "default") calls and +generates/updates en.json (the English source translation file). + +Usage: + python tools/extract-i18n.py # Preview (dry-run) + python tools/extract-i18n.py --write # Write en.json + python tools/extract-i18n.py --check # CI mode: exit 1 if en.json is stale + python tools/extract-i18n.py --orphans # List keys in en.json not found in code + +Workflow: + 1. Developer adds T("key", "English text") in source code + 2. Run: python tools/extract-i18n.py --write + 3. Commit en.json alongside the source change + 4. Weblate picks up new/changed keys automatically +""" + +import argparse +import json +import re +import sys +from pathlib import Path + + +def find_matching_paren(text: str, open_index: int) -> int: + """Find the matching ')' for text[open_index] == '(' while respecting strings.""" + depth = 0 + in_string = False + escape = False + + for index in range(open_index, len(text)): + char = text[index] + + if in_string: + if escape: + escape = False + elif char == "\\": + escape = True + elif char == '"': + in_string = False + continue + + if char == '"': + in_string = True + elif char == '(': + depth += 1 + elif char == ')': + depth -= 1 + if depth == 0: + return index + + return -1 + + +def split_top_level_args(arg_text: str) -> list[str]: + """Split a C++ argument list on top-level commas only.""" + args = [] + current = [] + paren_depth = 0 + brace_depth = 0 + bracket_depth = 0 + in_string = False + escape = False + + for char in arg_text: + if in_string: + current.append(char) + if escape: + escape = False + elif char == "\\": + escape = True + elif char == '"': + in_string = False + continue + + if char == '"': + in_string = True + current.append(char) + continue + + if char == '(': + paren_depth += 1 + elif char == ')': + paren_depth -= 1 + elif char == '{': + brace_depth += 1 + elif char == '}': + brace_depth -= 1 + elif char == '[': + bracket_depth += 1 + elif char == ']': + bracket_depth -= 1 + elif char == ',' and paren_depth == 0 and brace_depth == 0 and bracket_depth == 0: + args.append("".join(current).strip()) + current = [] + continue + + current.append(char) + + tail = "".join(current).strip() + if tail: + args.append(tail) + return args + + +def find_project_root(): + """Find the project root by looking for CMakeLists.txt.""" + d = Path(__file__).resolve().parent.parent + if (d / "CMakeLists.txt").exists(): + return d + d = Path.cwd() + while d != d.parent: + if (d / "CMakeLists.txt").exists(): + return d + d = d.parent + print("Error: Could not find project root (CMakeLists.txt)", file=sys.stderr) + sys.exit(1) + + +def strip_comments(source: str) -> str: + """Remove C/C++ line comments (//) and block comments (/* */).""" + # This regex handles string literals to avoid stripping "URLs with //" + # Pattern: match string literals, block comments, or line comments + pattern = re.compile( + r'("(?:[^"\\]|\\.)*")' # group 1: string literal — keep + r"|(/\*.*?\*/)" # group 2: block comment — remove + r"|(//[^\n]*)", # group 3: line comment — remove + re.DOTALL + ) + def replacer(m): + if m.group(1): + return m.group(1) # keep string literals + return " " # replace comments with space + return pattern.sub(replacer, source) + + +def extract_format_calls(clean: str, prefix: str) -> list[tuple[str, str]]: + """Extract Format(key, args, default) calls using balanced parsing instead of regex.""" + results = [] + + for match in re.finditer(r'->Format\s*\(', clean): + open_paren = match.end() - 1 + close_paren = find_matching_paren(clean, open_paren) + if close_paren == -1: + continue + + args = split_top_level_args(clean[open_paren + 1:close_paren]) + if len(args) < 3: + continue + + key_expr = args[0].strip() + default_expr = args[-1].strip() + + key_match = re.fullmatch(r'"([^"]+)"', key_expr) + tkey_match = re.fullmatch(r'TKEY\(\s*"([^"]+)"\s*\)', key_expr) + + if key_match: + key = key_match.group(1) + elif tkey_match and prefix: + key = prefix + tkey_match.group(1) + else: + continue + + if not re.fullmatch(r'(?:(?:"(?:[^"\\]|\\.)*"\s*)+)', default_expr, re.DOTALL): + continue + + results.append((key, default_expr)) + + return results + + +def extract_strings(src_dir: Path): + """ + Extract all T("key", "default") and Format("key", {...}, "default") strings. + Also handles TKEY("suffix") macro expansion via I18N_KEY_PREFIX. + Returns (dict of {key: default_text}, set of key-only keys). + """ + strings = {} + key_only = set() + conflicts = [] + + # T("key", "default text") + t_pattern = re.compile( + r'\bT\(\s*"([^"]+)"\s*,\s*' # T("key", + r'((?:"(?:[^"\\]|\\.)*"\s*)+)' # one or more adjacent "string" literals + r'\)', # ) + re.DOTALL + ) + + # T(TKEY("suffix"), "default text") + tkey_pattern = re.compile( + r'\bT\(\s*TKEY\(\s*"([^"]+)"\s*\)\s*,\s*' # T(TKEY("suffix"), + r'((?:"(?:[^"\\]|\\.)*"\s*)+)' # "default text" + r'\)', # ) + re.DOTALL + ) + + # T(TKEY("suffix")) — key-only with macro + tkey_keyonly_pattern = re.compile( + r'\bT\(\s*TKEY\(\s*"([^"]+)"\s*\)\s*\)', + re.DOTALL + ) + + # T("key") — key-only, no inline default + t_keyonly_pattern = re.compile( + r'\bT\(\s*"([^"]+)"\s*\)', + re.DOTALL + ) + + # #define I18N_KEY_PREFIX "prefix." + prefix_pattern = re.compile( + r'#\s*define\s+I18N_KEY_PREFIX\s+"([^"]+)"' + ) + + def concat_string_literals(raw: str) -> str: + """Parse concatenated C++ string literals: "a" "b" -> "ab" """ + parts = re.findall(r'"((?:[^"\\]|\\.)*)"', raw) + return unescape_cpp_string("".join(parts)) + + for ext in ("*.cpp", "*.h"): + for filepath in src_dir.rglob(ext): + try: + content = filepath.read_text(encoding="utf-8", errors="replace") + except Exception as e: + print(f"Warning: Could not read {filepath}: {e}", file=sys.stderr) + continue + + # Strip comments to avoid matching examples in doc comments + clean = strip_comments(content) + rel_path = filepath.relative_to(src_dir.parent) + + # Detect I18N_KEY_PREFIX in this file + prefix = "" + prefix_match = prefix_pattern.search(clean) + if prefix_match: + prefix = prefix_match.group(1) + + def add_string(key, default, source_path): + if key in strings and strings[key] != default: + conflicts.append((key, strings[key], default, str(source_path))) + strings[key] = default + # If this key was previously seen without a default, it's no longer "key-only" + if default and key in key_only: + key_only.discard(key) + + # Extract T("key", "default") calls (full key) + for match in t_pattern.finditer(clean): + key = match.group(1) + default = concat_string_literals(match.group(2)) + add_string(key, default, rel_path) + + # Extract T(TKEY("suffix"), "default") calls (prefix + suffix) + if prefix: + for match in tkey_pattern.finditer(clean): + key = prefix + match.group(1) + default = concat_string_literals(match.group(2)) + add_string(key, default, rel_path) + + # Extract ->Format(..., ..., "default") calls with balanced parsing. + for key, default_expr in extract_format_calls(clean, prefix): + default = concat_string_literals(default_expr) + add_string(key, default, rel_path) + + # Track T("key") key-only calls + for match in t_keyonly_pattern.finditer(clean): + key = match.group(1) + if key not in strings: + key_only.add(key) + + # Track T(TKEY("suffix")) key-only calls + if prefix: + for match in tkey_keyonly_pattern.finditer(clean): + key = prefix + match.group(1) + if key not in strings: + key_only.add(key) + + if conflicts: + for key, old, new, path in conflicts: + print(f"Warning: Key '{key}' has conflicting defaults:", file=sys.stderr) + print(f" Existing: {old!r}", file=sys.stderr) + print(f" New: {new!r} (in {path})", file=sys.stderr) + + return strings, key_only + + +def unescape_cpp_string(s: str) -> str: + """Unescape C++ string literal escape sequences.""" + s = s.replace("\\n", "\n") + s = s.replace("\\t", "\t") + s = s.replace('\\"', '"') + s = s.replace("\\\\", "\\") + s = re.sub(r"\\x([0-9a-fA-F]{2})", lambda m: chr(int(m.group(1), 16)), s) + return s + + +def load_existing_json(path: Path) -> dict: + """Load existing en.json, returning only string entries (skip _meta).""" + if not path.exists(): + return {} + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return {k: v for k, v in data.items() if k != "_meta" and isinstance(v, str)} + except Exception as e: + print(f"Warning: Could not read {path}: {e}", file=sys.stderr) + return {} + + +def build_output(strings: dict) -> dict: + """Build the final en.json content with _meta header.""" + output = { + "_meta": { + "language": "English", + "locale": "en", + "auto_generated": True, + "generator": "tools/extract-i18n.py", + "note": "DO NOT EDIT MANUALLY. Run: python tools/extract-i18n.py --write" + } + } + for key in sorted(strings.keys()): + output[key] = strings[key] + return output + + +def main(): + # Force UTF-8 output on Windows + if sys.stdout.encoding != "utf-8": + sys.stdout.reconfigure(encoding="utf-8") + if sys.stderr.encoding != "utf-8": + sys.stderr.reconfigure(encoding="utf-8") + + parser = argparse.ArgumentParser( + description="Extract i18n strings from Community Shaders source code." + ) + parser.add_argument("--write", action="store_true", + help="Write/update en.json (default: dry-run preview)") + parser.add_argument("--check", action="store_true", + help="CI mode: exit 1 if en.json is out of date") + parser.add_argument("--orphans", action="store_true", + help="List keys in en.json that are not in source code") + args = parser.parse_args() + + root = find_project_root() + src_dir = root / "src" + en_json_path = (root / "package" / "SKSE" / "Plugins" + / "CommunityShaders" / "Translations" / "en.json") + + print(f"Scanning: {src_dir}") + strings, key_only = extract_strings(src_dir) + + print(f"Found {len(strings)} strings with inline defaults") + if key_only: + print(f"Found {len(key_only)} key-only T() calls (no inline default):") + for k in sorted(key_only): + print(f" - {k}") + + if args.orphans: + existing = load_existing_json(en_json_path) + orphans = set(existing.keys()) - set(strings.keys()) + if orphans: + print(f"\n{len(orphans)} orphaned key(s) in en.json (not found in source):") + for k in sorted(orphans): + print(f" - {k}") + else: + print("\nNo orphaned keys found.") + return + + output = build_output(strings) + output_text = json.dumps(output, indent=4, ensure_ascii=False) + "\n" + + if args.check: + if en_json_path.exists(): + existing_text = en_json_path.read_text(encoding="utf-8") + if existing_text == output_text: + print("en.json is up to date.") + sys.exit(0) + else: + print("en.json is OUT OF DATE. Run: python tools/extract-i18n.py --write", + file=sys.stderr) + existing = load_existing_json(en_json_path) + added = set(strings.keys()) - set(existing.keys()) + removed = set(existing.keys()) - set(strings.keys()) + changed = {k for k in strings if k in existing and strings[k] != existing[k]} + if added: + print(f" Added: {', '.join(sorted(added))}") + if removed: + print(f" Removed: {', '.join(sorted(removed))}") + if changed: + print(f" Changed: {', '.join(sorted(changed))}") + sys.exit(1) + else: + print("en.json does not exist. Run: python tools/extract-i18n.py --write", + file=sys.stderr) + sys.exit(1) + + if args.write: + en_json_path.parent.mkdir(parents=True, exist_ok=True) + with open(en_json_path, "w", encoding="utf-8", newline="\n") as f: + f.write(output_text) + print(f"Wrote {len(strings)} strings to {en_json_path}") + else: + print(f"\nPreview of en.json ({len(strings)} strings):") + print("-" * 60) + for key in sorted(strings.keys()): + val = strings[key] + display = val.replace("\n", "\\n") + if len(display) > 80: + display = display[:77] + "..." + print(f" {key}: {display!r}") + print("-" * 60) + print(f"Use --write to generate {en_json_path}") + + +if __name__ == "__main__": + main() From 03096bed3cb72356e67758d77a78e90b66508d37 Mon Sep 17 00:00:00 2001 From: Dlizzio <77717521+Dlizzio@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:59:29 -0700 Subject: [PATCH 14/55] chore(UI): centeralize rounded button highlight (#2454) --- .../CommunityShaders/Translations/en.json | 1 + src/CSEditor/EditorWindow.cpp | 10 +- src/Features/CSEditor.cpp | 2 +- src/Features/PerformanceOverlay.cpp | 2 +- src/Menu.cpp | 2 +- src/Menu/FeatureListRenderer.cpp | 8 +- src/Menu/MenuHeaderRenderer.cpp | 13 +- src/Utils/UI.cpp | 197 +++++++++++++----- src/Utils/UI.h | 12 +- 9 files changed, 170 insertions(+), 77 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index b8dbab9f52..665ff94814 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1729,6 +1729,7 @@ "menu.features.scene_specific_settings": "Scene Specific Settings", "menu.features.select_feature_left": "Please select a feature from the left.", "menu.features.select_item_left": "Please select an item on the left.", + "menu.features.setting_change_warning_title": "Setting Change Warning", "menu.features.settings_adjusted_warning": "Some of your settings have been automatically adjusted due to feature incompatibilities.", "menu.features.settings_hidden_disabled": "Feature settings are hidden because this feature is disabled at boot.", "menu.features.unloaded_features": "Unloaded Features", diff --git a/src/CSEditor/EditorWindow.cpp b/src/CSEditor/EditorWindow.cpp index b5c5b050d1..fce755896e 100644 --- a/src/CSEditor/EditorWindow.cpp +++ b/src/CSEditor/EditorWindow.cpp @@ -93,16 +93,12 @@ bool IconButton(const char* label, bool filled, const char* iconType) bool result = ImGui::InvisibleButton(label, buttonSize); - bool hovered = ImGui::IsItemHovered(); - bool active = ImGui::IsItemActive(); - - ImU32 bgColor = active ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : - hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : - ImGui::GetColorU32(ImGuiCol_Button); ImU32 iconColor = ImGui::GetColorU32(ImGuiCol_Text); auto* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(cursorPos, ImVec2(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y), bgColor, ImGui::GetStyle().FrameRounding); + const ImVec2 buttonMax(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y); + drawList->AddRectFilled(cursorPos, buttonMax, ImGui::GetColorU32(ImGuiCol_Button), ImGui::GetStyle().FrameRounding); + Util::DrawCurrentItemRoundedButtonHighlight(drawList); ImVec2 center(cursorPos.x + buttonSize.x * 0.5f, cursorPos.y + buttonSize.y * 0.5f); float iconSize = buttonSize.x * 0.35f; diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index f78b53119a..d6c87c1392 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -392,7 +392,7 @@ void CSEditor::RenderWeatherDetailsWindow(bool* open) } ImGui::SetNextWindowSize(ImVec2(600 * scale, 800 * scale), ImGuiCond_FirstUseEver); - if (ImGui::Begin("Weather Details##Popup", open, ImGuiWindowFlags_None)) { + if (Util::BeginWithRoundedClose("Weather Details##Popup", open, ImGuiWindowFlags_None)) { // Remember window position for next frame ImVec2 currentPos = ImGui::GetWindowPos(); if (currentPos.x != WeatherDetailsWindow.Position.x || currentPos.y != WeatherDetailsWindow.Position.y) { diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index 87e1c5a840..cf4c8421a1 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -344,7 +344,7 @@ void PerformanceOverlay::DrawOverlay() } // Create the window - ImGui::Begin(T(TKEY("overlay_title"), "Performance Overlay"), NULL, windowFlags); + Util::BeginWithRoundedClose(T(TKEY("overlay_title"), "Performance Overlay"), nullptr, windowFlags); // Remember window position for next frame if (ImGui::IsWindowAppearing()) { diff --git a/src/Menu.cpp b/src/Menu.cpp index eecdca93da..99e716d600 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -685,7 +685,7 @@ void Menu::DrawSettings() windowFlags |= ImGuiWindowFlags_NoTitleBar; } - ImGui::Begin(title.c_str(), &IsEnabled, windowFlags); + Util::BeginWithRoundedClose(title.c_str(), &IsEnabled, windowFlags); { // Update docking state tracking bool isDocked = ImGui::IsWindowDocked(); diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index e52012bd3c..e1c0fabdef 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -23,6 +23,7 @@ #include "SettingsOverrideManager.h" #include "State.h" #include "Util.h" +#include "Utils/UI.h" #include "WeatherVariableRegistry.h" namespace @@ -942,16 +943,19 @@ void FeatureListRenderer::DrawMenuVisitor::RenderReactiveConstraintWarningDialog return; } + constexpr const char* popupId = "###SettingChangeWarning"; + const std::string popupTitle = fmt::format("{}{}", T("menu.features.setting_change_warning_title", "Setting Change Warning"), popupId); + // OpenPopup is idempotent while the popup is already open, so calling it // every frame while the flag is set is safe and ensures we don't miss the // one-frame window where ImGui expects it. - ImGui::OpenPopup("Setting Change Warning"); + ImGui::OpenPopup(popupId); // Center the popup (ImGuiCond_Always matches the Clear Cache dialog pattern) ImVec2 center = ImGui::GetMainViewport()->GetCenter(); ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - if (ImGui::BeginPopupModal("Setting Change Warning", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + if (Util::BeginPopupModalWithRoundedClose(popupTitle.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::TextWrapped("%s", T("menu.features.settings_adjusted_warning", "Some of your settings have been automatically adjusted due to feature incompatibilities.")); ImGui::Spacing(); ImGui::Separator(); diff --git a/src/Menu/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 5b48a79131..c7fd4e32e7 100644 --- a/src/Menu/MenuHeaderRenderer.cpp +++ b/src/Menu/MenuHeaderRenderer.cpp @@ -313,13 +313,11 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector& action ImVec2 iconMax(iconX + iconSize - paddingReduction, iconY + iconSize - paddingReduction); // Use the full area for mouse interaction (including padding) - ImVec2 interactionMin(iconX, iconY); - ImVec2 interactionMax(iconX + iconSize, iconY + iconSize); + ImRect interactionRect({ iconX, iconY }, { iconX + iconSize, iconY + iconSize }); // Check mouse interaction against full area - ImVec2 mousePos = ImGui::GetMousePos(); - bool isHovered = mousePos.x >= interactionMin.x && mousePos.x <= interactionMax.x && - mousePos.y >= interactionMin.y && mousePos.y <= interactionMax.y; + const bool isHovered = ImGui::IsMouseHoveringRect(interactionRect.Min, interactionRect.Max, false); + Util::DrawRoundedButtonHighlight(interactionRect, isHovered, isHovered && ImGui::IsMouseDown(ImGuiMouseButton_Left), fgDrawList); // Only render if texture is valid if (it->texture) { @@ -341,9 +339,6 @@ void MenuHeaderRenderer::RenderDockedIcons(const std::vector& action // Handle interaction if (isHovered) { - // Draw subtle background for hovered icon using interaction area - fgDrawList->AddRectFilled(interactionMin, interactionMax, IM_COL32(255, 255, 255, 40)); - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { it->callback(); } @@ -446,4 +441,4 @@ void MenuHeaderRenderer::RenderWatermarkLogo(const Menu::UIIcons& uiIcons) } drawList->AddImage(uiIcons.logo.texture, logoMin, logoMax, ImVec2(0, 0), ImVec2(1, 1), watermarkColor); -} \ No newline at end of file +} diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index f23dd9b7e6..b60471bd8e 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -126,7 +127,7 @@ namespace Util // measurement frame, causing TextWrapped to wrap at 0px and produce an enormous height. // Setting an initial width gives TextWrapped a sensible wrap column on that frame. ImGui::SetNextWindowSize(ImVec2(400.0f * GetUIScale(), 0.0f), ImGuiCond_Appearing); - isOpen = ImGui::BeginPopupModal(name, p_open, flags | ImGuiWindowFlags_NoSavedSettings); + isOpen = BeginPopupModalWithRoundedClose(name, p_open, flags | ImGuiWindowFlags_NoSavedSettings); } CenteredPopupModal::~CenteredPopupModal() @@ -619,96 +620,182 @@ namespace Util return theme.UseMonochromeIcons ? theme.Palette.Text : ImVec4(1, 1, 1, 1); } + static float GetPillRounding(const ImVec2& min, const ImVec2& max) + { + IM_ASSERT(max.x >= min.x && max.y >= min.y); + return ImMin(max.x - min.x, max.y - min.y) * 0.5f; + } + + static float GetThemedButtonHighlightRounding(const ImVec2& min, const ImVec2& max) + { + const float frameRounding = ImGui::GetStyle().FrameRounding; + IM_ASSERT(frameRounding >= 0.0f); + return ImMin(ImMax(frameRounding, 0.0f), GetPillRounding(min, max)); + } + + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(min, max, hovered, active, GetThemedButtonHighlightRounding(min, max), drawList); + } + + bool DrawRoundedButtonHighlight(const ImRect& rect, bool hovered, bool active, ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(rect.Min, rect.Max, hovered, active, drawList); + } + + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, float rounding, ImDrawList* drawList) + { + if (!hovered && !active) + return false; + + IM_ASSERT(max.x >= min.x && max.y >= min.y); + IM_ASSERT(rounding >= 0.0f); + if (!drawList) + drawList = ImGui::GetWindowDrawList(); + + drawList->AddRectFilled(min, max, ImGui::GetColorU32(active ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), rounding); + return true; + } + + bool DrawCurrentItemRoundedButtonHighlight(ImDrawList* drawList) + { + return DrawRoundedButtonHighlight(ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), ImGui::IsItemHovered(), ImGui::IsItemActive(), drawList); + } + // Shared constants for title-bar button overlays - static constexpr float kButtonPad = 2.0f; // extra padding around hit/highlight area - static constexpr float kCrossDiag = 0.5f * 0.7071f; // half-size * 1/sqrt(2) for cross line endpoints - static constexpr float kCrossInset = 1.0f; // inward inset so cross doesn't touch edges + static constexpr float kTitleBarButtonPadding = 2.0f; + static constexpr float kCloseCrossDiagonalScale = 0.5f / std::numbers::sqrt2_v; + static constexpr float kCloseCrossInset = 1.0f; + static constexpr ImVec4 kTransparentButtonChrome(0, 0, 0, 0); - // Compute the bounding rect for a title-bar button of font-sized square + padding. - static ImRect ButtonBB(const ImVec2& origin, float fontSize) + static ImRect TitleBarButtonRect(const ImVec2& origin, float fontSize) { - const float full = fontSize + kButtonPad * 2.0f; + const float full = fontSize + kTitleBarButtonPadding * 2.0f; return ImRect(origin, ImVec2(origin.x + full, origin.y + full)); } - // Draws a rounded highlight overlay for a title bar button. - static void DrawRoundedButtonHighlight(ImGuiWindow* window, const ImRect& bb, float rounding) + static ImVec2 RightTitleBarButtonOrigin(ImGuiWindow* window, float fontSize, float offset = 0.0f) { - ImGuiContext& g = *ImGui::GetCurrentContext(); - bool isTop = (g.HoveredWindow == window); - bool hovered = isTop && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); - bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); - if (hovered || held) - window->DrawList->AddRectFilled(bb.Min, bb.Max, ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), rounding); + const auto& style = ImGui::GetStyle(); + return ImVec2(window->Rect().Max.x - window->WindowBorderSize - style.FramePadding.x - fontSize - offset - kTitleBarButtonPadding, + window->Rect().Min.y + style.FramePadding.y - kTitleBarButtonPadding); } - // Draws a rounded close button overlay, matching native ImGui CloseButton position. - static void DrawRoundedCloseButton(ImGuiWindow* window, bool* p_open) + static ImVec2 CollapseTitleBarButtonOrigin(ImGuiWindow* window, bool hasCloseButton, float fontSize) { const auto& style = ImGui::GetStyle(); - const float sz = ImGui::GetFontSize(); - const ImVec2 pos(window->Rect().Max.x - window->WindowBorderSize - style.FramePadding.x - sz - kButtonPad, - window->Rect().Min.y + style.FramePadding.y - kButtonPad); - const ImRect bb = ButtonBB(pos, sz); - const float rounding = (sz + kButtonPad * 2.0f) * 0.5f; + IM_ASSERT(style.WindowMenuButtonPosition == ImGuiDir_Left || style.WindowMenuButtonPosition == ImGuiDir_Right); + + if (style.WindowMenuButtonPosition == ImGuiDir_Right) + return RightTitleBarButtonOrigin(window, fontSize, hasCloseButton ? fontSize : 0.0f); + return ImVec2(window->Pos.x + window->WindowBorderSize + style.FramePadding.x - kTitleBarButtonPadding, + window->Pos.y + style.FramePadding.y - kTitleBarButtonPadding); + } + + static bool IsTitleBarButtonHovered(ImGuiWindow* window, const ImRect& bb) + { ImGuiContext& g = *ImGui::GetCurrentContext(); - bool isTop = (g.HoveredWindow == window); - bool hovered = isTop && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); + return g.HoveredWindow == window && ImGui::IsMouseHoveringRect(bb.Min, bb.Max, false); + } + + class NativeTitleBarButtonHighlightGuard + { + public: + NativeTitleBarButtonHighlightGuard() + { + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kTransparentButtonChrome); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, kTransparentButtonChrome); + } + + ~NativeTitleBarButtonHighlightGuard() { ImGui::PopStyleColor(2); } + }; + + // Draws a rounded close button overlay, matching native ImGui CloseButton position. + static void DrawRoundedCloseHighlight(ImGuiWindow* window) + { + if (window->Flags & ImGuiWindowFlags_NoTitleBar) + return; + + const float sz = ImGui::GetFontSize(); + const ImVec2 pos = RightTitleBarButtonOrigin(window, sz); + const ImRect bb = TitleBarButtonRect(pos, sz); + const bool hovered = IsTitleBarButtonHovered(window, bb); + const bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); window->DrawList->PushClipRect(window->Rect().Min, window->Rect().Max); - DrawRoundedButtonHighlight(window, bb, rounding); - - // Cross lines — matches ImGui's internal RenderCloseButton geometry - const ImVec2 c = bb.GetCenter(); - const float d = sz * kCrossDiag - kCrossInset; - const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); - window->DrawList->AddLine({ c.x - d, c.y - d }, { c.x + d, c.y + d }, col); - window->DrawList->AddLine({ c.x + d, c.y - d }, { c.x - d, c.y + d }, col); - window->DrawList->PopClipRect(); + const bool highlighted = DrawRoundedButtonHighlight(bb, hovered, held, window->DrawList); - if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - *p_open = false; + // Cross lines match ImGui's internal RenderCloseButton geometry. + if (highlighted) { + const ImVec2 c = bb.GetCenter(); + const float d = sz * kCloseCrossDiagonalScale - kCloseCrossInset; + const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); + window->DrawList->AddLine({ c.x - d, c.y - d }, { c.x + d, c.y + d }, col); + window->DrawList->AddLine({ c.x + d, c.y - d }, { c.x - d, c.y + d }, col); + } + window->DrawList->PopClipRect(); } // Draws a rounded highlight for the collapse/triangle button in the title bar. - static void DrawRoundedCollapseHighlight(ImGuiWindow* window) + static void DrawRoundedCollapseHighlight(ImGuiWindow* window, bool hasCloseButton) { + if (window->Flags & ImGuiWindowFlags_NoTitleBar) + return; if (window->Flags & ImGuiWindowFlags_NoCollapse) return; if (ImGui::GetStyle().WindowMenuButtonPosition == ImGuiDir_None) return; - const auto& style = ImGui::GetStyle(); const float sz = ImGui::GetFontSize(); - const ImVec2 pos(window->Pos.x + window->WindowBorderSize + style.FramePadding.x - kButtonPad, - window->Pos.y + style.FramePadding.y - kButtonPad); - const ImRect bb = ButtonBB(pos, sz); - const float rounding = (sz + kButtonPad * 2.0f) * 0.5f; + const ImVec2 pos = CollapseTitleBarButtonOrigin(window, hasCloseButton, sz); + const ImRect bb = TitleBarButtonRect(pos, sz); + const bool hovered = IsTitleBarButtonHovered(window, bb); + const bool held = hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left); window->DrawList->PushClipRect(window->Rect().Min, window->Rect().Max); - DrawRoundedButtonHighlight(window, bb, rounding); + const bool highlighted = DrawRoundedButtonHighlight(bb, hovered, held, window->DrawList); - // Redraw the triangle arrow on top of the highlight so it stays visible - const ImVec2 arrowPos(pos.x + kButtonPad, pos.y + kButtonPad); - const ImGuiDir dir = window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down; - ImGui::RenderArrow(window->DrawList, arrowPos, ImGui::GetColorU32(ImGuiCol_Text), dir, 1.0f); + if (highlighted) { + const ImVec2 arrowPos(pos.x + kTitleBarButtonPadding, pos.y + kTitleBarButtonPadding); + const ImGuiDir dir = window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down; + ImGui::RenderArrow(window->DrawList, arrowPos, ImGui::GetColorU32(ImGuiCol_Text), dir, 1.0f); + } window->DrawList->PopClipRect(); } + static void DrawRoundedTitleBarButtonHighlights(ImGuiWindow* window, bool hasCloseButton, bool hasCollapseButton) + { + if (!window) + return; + + if (hasCollapseButton) + DrawRoundedCollapseHighlight(window, hasCloseButton); + if (hasCloseButton) + DrawRoundedCloseHighlight(window); + } + bool BeginWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags) { - // Hide native sharp-cornered highlights; we draw rounded ones after Begin() - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0)); - bool visible = ImGui::Begin(name, p_open, flags); - ImGui::PopStyleColor(2); - if (auto* window = ImGui::GetCurrentWindowRead()) { - DrawRoundedCollapseHighlight(window); - if (p_open) - DrawRoundedCloseButton(window, p_open); + bool visible = false; + { + NativeTitleBarButtonHighlightGuard guard; + visible = ImGui::Begin(name, p_open, flags); + } + DrawRoundedTitleBarButtonHighlights(ImGui::GetCurrentWindowRead(), p_open != nullptr, true); + return visible; + } + + bool BeginPopupModalWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags) + { + bool visible = false; + { + NativeTitleBarButtonHighlightGuard guard; + visible = ImGui::BeginPopupModal(name, p_open, flags); } + if (visible) + DrawRoundedTitleBarButtonHighlights(ImGui::GetCurrentWindowRead(), p_open != nullptr, false); return visible; } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 5848aa470a..e81bad9c23 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -15,6 +15,7 @@ // Forward declarations struct ID3D11Device; struct ID3D11ShaderResourceView; +struct ImRect; struct ImVec2; class Menu; class Feature; @@ -319,8 +320,17 @@ namespace Util /** Returns theme text color if monochrome icons enabled, otherwise white. */ ImVec4 GetIconTint(); - /// ImGui::Begin() wrapper that replaces the native close button with a rounded one. + /// Draws a theme-rounded hover/active fill over a button rect. + bool DrawRoundedButtonHighlight(const ImRect& rect, bool hovered, bool active, ImDrawList* drawList = nullptr); + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, ImDrawList* drawList = nullptr); + bool DrawRoundedButtonHighlight(const ImVec2& min, const ImVec2& max, bool hovered, bool active, float rounding, ImDrawList* drawList); + + /// Draws the rounded hover/active fill for the last submitted item. + bool DrawCurrentItemRoundedButtonHighlight(ImDrawList* drawList = nullptr); + + /// ImGui::Begin() wrappers that replace native title-bar button highlights with rounded ones. bool BeginWithRoundedClose(const char* name, bool* p_open, ImGuiWindowFlags flags = 0); + bool BeginPopupModalWithRoundedClose(const char* name, bool* p_open = nullptr, ImGuiWindowFlags flags = 0); /** * Button with simple flash feedback (matches action icon hover effect style) From 15e45755bc515a49fff8535e88101da60c730116 Mon Sep 17 00:00:00 2001 From: Dlizzio <77717521+Dlizzio@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:26:11 -0700 Subject: [PATCH 15/55] chore(UI): weather picker refinement (#2456) --- .../CommunityShaders/Translations/en.json | 10 +- .../CommunityShaders/Translations/zh_CN.json | 10 +- src/Features/CSEditor.cpp | 91 ++++++++++--------- src/Features/CSEditor.h | 9 +- src/Features/WetnessEffects.cpp | 30 +++--- src/Menu.cpp | 2 +- src/Menu/FeatureListRenderer.cpp | 7 +- 7 files changed, 88 insertions(+), 71 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 665ff94814..7ae1794d11 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -517,7 +517,6 @@ "feature.cs_editor.thunder_freq_info_7": "Range: 0-255 (unsigned 8-bit integer)", "feature.cs_editor.thunder_freq_info_8": "Note: Creation Kit interprets this value non-linearly", "feature.cs_editor.thunder_frequency": "Thunder Frequency: %u", - "feature.cs_editor.time_controls": "Time Controls", "feature.cs_editor.toggle_with": "Toggle with ", "feature.cs_editor.tooltip_editor_id": "Editor ID: %s", "feature.cs_editor.tooltip_editor_id_2": "Editor ID: %s", @@ -534,10 +533,9 @@ "feature.cs_editor.using_default_settings": "Using Default Settings", "feature.cs_editor.weather": "Weather", "feature.cs_editor.weather_controls": "Weather Controls", - "feature.cs_editor.weather_details": "Weather Details", "feature.cs_editor.weather_information": "Weather Information", "feature.cs_editor.weather_percentage": "Weather Percentage: %.1f%%", - "feature.cs_editor.weather_status": "Weather Status", + "feature.cs_editor.weather_picker": "Weather Picker", "feature.cs_editor.weather_wind_speed": "Weather Wind Speed: %.2f (raw %d)", "feature.cs_editor.wind_direction": "Wind Direction: %.1f° (raw %d)", "feature.cs_editor.wind_direction_range": "Wind Direction Range: %.1f° (raw %d)", @@ -1491,6 +1489,7 @@ "feature.wetness_effects.climate_preset_nordic_desc": "Balanced Nordic climate (moderate rain)", "feature.wetness_effects.climate_preset_unknown": "Unknown", "feature.wetness_effects.climate_presets": "Climate Presets", + "feature.wetness_effects.current_climate_preset": "Current Climate Preset", "feature.wetness_effects.custom_preset_tooltip_0": "Custom settings - you have modified the preset values.", "feature.wetness_effects.custom_preset_tooltip_1": "Select a preset above to apply predefined climate settings.", "feature.wetness_effects.debug": "Debug", @@ -1529,8 +1528,8 @@ "feature.wetness_effects.min_rain_wetness": "Min Rain Wetness", "feature.wetness_effects.min_rain_wetness_tooltip": "The minimum amount an object gets wet from rain.", "feature.wetness_effects.name": "Wetness Effects", - "feature.wetness_effects.open_feature": "Open {}", - "feature.wetness_effects.open_installed_feature_tooltip": "Open the installed %s feature", + "feature.wetness_effects.open_weather_picker": "Open Weather Picker", + "feature.wetness_effects.open_weather_picker_tooltip": "Open the Weather Picker in CS Utility", "feature.wetness_effects.portion_of_grid_size": "As portion of grid size.", "feature.wetness_effects.puddle_max_angle": "Puddle Max Angle", "feature.wetness_effects.puddle_max_angle_tooltip": "How flat a surface needs to be for puddles to form on it.", @@ -1542,6 +1541,7 @@ "feature.wetness_effects.puddle_wetness_in_exterior": "Puddle Wetness In/Exterior", "feature.wetness_effects.radius": "Radius", "feature.wetness_effects.rain_in_exterior": "Rain In/Exterior", + "feature.wetness_effects.rain_system_state": "Rain System State", "feature.wetness_effects.rain_wetness": "Rain Wetness", "feature.wetness_effects.raindrop_effects": "Raindrop Effects", "feature.wetness_effects.raindrops": "Raindrops", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index bdcf9974dc..0f38a04c23 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -906,6 +906,7 @@ "feature.wetness_effects.climate_preset_nordic": "北欧(默认)", "feature.wetness_effects.climate_preset_nordic_desc": "平衡的北欧气候(中雨)", "feature.wetness_effects.climate_preset_unknown": "未知", + "feature.wetness_effects.current_climate_preset": "当前气候预设", "feature.wetness_effects.climate_presets": "气候预设", "feature.wetness_effects.custom_preset_tooltip_0": "自定义设置 - 您已修改预设值。", "feature.wetness_effects.custom_preset_tooltip_1": "在上方选择一个预设以应用预定义的气候设置。", @@ -945,8 +946,8 @@ "feature.wetness_effects.min_rain_wetness": "最小降雨湿润度", "feature.wetness_effects.min_rain_wetness_tooltip": "物体因雨水变湿的最小程度。", "feature.wetness_effects.name": "湿润效果", - "feature.wetness_effects.open_feature": "打开 {}", - "feature.wetness_effects.open_installed_feature_tooltip": "打开已安装的 %s 功能", + "feature.wetness_effects.open_weather_picker": "打开天气选择器", + "feature.wetness_effects.open_weather_picker_tooltip": "在 CS 实用工具中打开天气选择器", "feature.wetness_effects.portion_of_grid_size": "作为网格尺寸的比例。", "feature.wetness_effects.puddle_max_angle": "积水最大角度", "feature.wetness_effects.puddle_max_angle_tooltip": "表面需要多平才能形成积水。", @@ -962,6 +963,7 @@ "feature.wetness_effects.raindrop_effects": "雨滴效果", "feature.wetness_effects.raindrops": "雨滴", "feature.wetness_effects.raindrops_help": "在每个间隔内,每个网格单元中放置一个雨滴。\n只有设定比例的雨滴会实际触发飞溅和涟漪。\n", + "feature.wetness_effects.rain_system_state": "雨水系统状态", "feature.wetness_effects.ripples": "涟漪", "feature.wetness_effects.shore_range": "岸边范围", "feature.wetness_effects.shore_range_tooltip": "岸边湿润效果影响水体的最大距离", @@ -2056,7 +2058,6 @@ "feature.cs_editor.thunder_freq_info_7": "范围:0-255(无符号8位整数)", "feature.cs_editor.thunder_freq_info_8": "注意:Creation Kit以非线性方式解释此值", "feature.cs_editor.thunder_frequency": "雷声频率:%u", - "feature.cs_editor.time_controls": "时间控制", "feature.cs_editor.toggle_with": "切换键:", "feature.cs_editor.tooltip_editor_id": "编辑器ID:%s", "feature.cs_editor.tooltip_editor_id_2": "编辑器ID:%s", @@ -2073,10 +2074,9 @@ "feature.cs_editor.using_default_settings": "使用默认设置", "feature.cs_editor.weather": "天气", "feature.cs_editor.weather_controls": "天气控制", - "feature.cs_editor.weather_details": "天气详情", "feature.cs_editor.weather_information": "天气信息", "feature.cs_editor.weather_percentage": "天气百分比:%.1f%%", - "feature.cs_editor.weather_status": "天气状态", + "feature.cs_editor.weather_picker": "天气选择器", "feature.cs_editor.weather_wind_speed": "天气风速:%.2f(原始%d)", "feature.cs_editor.wind_direction": "风向:%.1f°(原始%d)", "feature.cs_editor.wind_direction_range": "风向范围:%.1f°(原始%d)", diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index d6c87c1392..98e2368e50 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -167,6 +167,9 @@ void CSEditor::DrawSettings() OpenEditorWindow(); ImGui::EndDisabled(); + ImGui::Spacing(); + ImGui::SeparatorText(T(TKEY("weather_picker"), "Weather Picker")); + // Time controls DrawTimeControls(); @@ -175,6 +178,27 @@ void CSEditor::DrawSettings() // Integrated Weather Picker UI DrawWeatherPickerSection(); + + ImGui::Spacing(); + DrawShowInOverlayToggle(); +} + +void CSEditor::DrawShowInOverlayToggle() +{ + const auto& themeSettings = Menu::GetSingleton()->GetTheme(); + const auto& menuSettings = Menu::GetSingleton()->GetSettings(); + + bool showInOverlay = WeatherDetailsWindow.ShowInOverlay; + if (ImGui::Checkbox(T(TKEY("show_in_overlay"), "Show in Overlay"), &showInOverlay)) { + WeatherDetailsWindow.ShowInOverlay = showInOverlay; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("show_in_overlay_tooltip"), + "Opens weather details in a separate window that stays open\neven when the main menu is closed. ")); + ImGui::Text(T(TKEY("toggle_with"), "Toggle with ")); + ImGui::SameLine(); + ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.OverlayToggleKey).c_str()); + } } void CSEditor::Prepass() @@ -200,27 +224,9 @@ void CSEditor::Prepass() void CSEditor::DrawWeatherPickerSection() { ImGui::Spacing(); - Util::DrawSectionHeader(T(TKEY("weather_details"), "Weather Details")); - - const auto& themeSettings = Menu::GetSingleton()->GetTheme(); - const auto& menuSettings = Menu::GetSingleton()->GetSettings(); - - // Show as Overlay checkbox - bool showInOverlay = WeatherDetailsWindow.ShowInOverlay; - if (ImGui::Checkbox(T(TKEY("show_in_overlay"), "Show in Overlay"), &showInOverlay)) { - WeatherDetailsWindow.ShowInOverlay = showInOverlay; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", T(TKEY("show_in_overlay_tooltip"), - "Opens weather details in a separate window that stays open\neven when the main menu is closed. ")); - ImGui::Text(T(TKEY("toggle_with"), "Toggle with ")); - ImGui::SameLine(); - ImGui::TextColored(themeSettings.StatusPalette.CurrentHotkey, "%s", Util::Input::KeyIdToString(menuSettings.OverlayToggleKey).c_str()); - } - ImGui::Spacing(); // Render core weather details - RenderCoreWeatherDetails(true); // true = show interactive elements in main settings panel + RenderCoreWeatherDetails(true, false); // true = show interactive elements in main settings panel // Render weather analysis from features with collapsible headers RenderFeatureWeatherAnalysis(); @@ -301,8 +307,6 @@ void CSEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeathe void CSEditor::DrawTimeControls() { - ImGui::Spacing(); - Util::DrawSectionHeader(T(TKEY("time_controls"), "Time Controls")); ImGui::Spacing(); EditorWindow::GetSingleton()->DrawTimeControls(); ImGui::Spacing(); @@ -311,8 +315,6 @@ void CSEditor::DrawTimeControls() void CSEditor::DrawWeatherStatusPanel() { ImGui::Spacing(); - Util::DrawSectionHeader(T(TKEY("weather_status"), "Weather Status")); - ImGui::Spacing(); auto weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); @@ -371,7 +373,7 @@ void CSEditor::DrawWeatherStatusPanel() // Weather Picker functionality (integrated from WeatherPicker feature) // ================================================================================ -void CSEditor::RenderWeatherDetailsWindow(bool* open) +void CSEditor::RenderWeatherDetailsWindow(bool* open, bool showSectionHeaders) { if (!open || !*open) return; @@ -405,7 +407,7 @@ void CSEditor::RenderWeatherDetailsWindow(bool* open) (globals::game::ui && globals::game::ui->IsMenuOpen(RE::CursorMenu::MENU_NAME))); }; - RenderCoreWeatherDetails(shouldEnableInteractiveElements()); + RenderCoreWeatherDetails(shouldEnableInteractiveElements(), showSectionHeaders); // Render weather analysis from features with collapsible headers RenderFeatureWeatherAnalysis(); @@ -635,14 +637,16 @@ void CSEditor::DisplayWeatherInfo(RE::TESWeather* weather, float weatherPct, boo CSEditor::DisplayWindInfo(weather); } -void CSEditor::RenderWeatherControls(RE::Sky* sky) +void CSEditor::RenderWeatherControls(RE::Sky* sky, bool showSectionHeader) { // Weather Selection Section (only show interactive elements in inline mode) static bool weatherControlsExpanded = true; - Util::DrawSectionHeader(T(TKEY("weather_controls"), "Weather Controls"), false, true, &weatherControlsExpanded); + if (showSectionHeader) { + Util::DrawSectionHeader(T(TKEY("weather_controls"), "Weather Controls"), false, true, &weatherControlsExpanded); - if (!weatherControlsExpanded) - return; + if (!weatherControlsExpanded) + return; + } ImGui::Text("%s", T(TKEY("filter_by_weather_type"), "Filter by Weather Type:")); if (ImGui::Button(T(TKEY("select_all"), "Select All"))) { @@ -820,12 +824,14 @@ void CSEditor::RenderWeatherControls(RE::Sky* sky) } } -void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) +void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements, bool showSectionHeader) { ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing(); - Util::DrawSectionHeader(T(TKEY("weather_information"), "Weather Information"), false, true); + if (showSectionHeader) { + Util::DrawSectionHeader(T(TKEY("weather_information"), "Weather Information"), false, true); + } // Update cache: store current lastWeather if it exists, otherwise keep the cached one if (sky->lastWeather) { @@ -836,7 +842,7 @@ void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiv RE::TESWeather* displayLastWeather = sky->lastWeather ? sky->lastWeather : s_cachedLastWeather; // Create resizable 2-column table for current and last weather - if (ImGui::BeginTable("WeatherComparison", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) { + if (ImGui::BeginTable("WeatherComparison", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) { // Set up columns ImGui::TableSetupColumn(T(TKEY("current_weather_column"), "Current Weather"), ImGuiTableColumnFlags_WidthStretch, 0.5f); ImGui::TableSetupColumn(T(TKEY("last_weather_column"), "Last Weather"), ImGuiTableColumnFlags_WidthStretch, 0.5f); @@ -856,7 +862,7 @@ void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiv } } -void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements) +void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements, bool showSectionHeaders) { const auto showError = [](const char* msg) { auto menu = Menu::GetSingleton(); @@ -867,9 +873,9 @@ void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements) if (auto sky = globals::game::sky) { if (sky->mode.get() == RE::Sky::Mode::kFull) { if (showInteractiveElements) { - RenderWeatherControls(sky); + RenderWeatherControls(sky, showSectionHeaders); } - RenderWeatherInformationDisplay(sky, showInteractiveElements); + RenderWeatherInformationDisplay(sky, showInteractiveElements, showSectionHeaders); ImGui::Spacing(); } else { showError(T(TKEY("sky_not_full"), "Sky not in full mode")); @@ -973,8 +979,8 @@ void CSEditor::RenderFeatureWeatherAnalysis() auto featureName = feature->GetShortName(); ImGui::PushID(featureName.c_str()); - // Create collapsible header for feature weather analysis - bool isExpanded = ImGui::CollapsingHeader(weatherConfig.sectionName.c_str(), ImGuiTreeNodeFlags_DefaultOpen); + const ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth; + bool isExpanded = ImGui::TreeNodeEx(weatherConfig.sectionName.c_str(), treeFlags); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", T(TKEY("feature_weather_analysis_tooltip_0"), "Weather analysis provided by: ")); ImGui::Text("%s", feature->GetDisplayName().c_str()); @@ -984,9 +990,12 @@ void CSEditor::RenderFeatureWeatherAnalysis() isExpanded ? T(TKEY("collapse"), "collapse") : T(TKEY("expand"), "expand")); } - if (isExpanded && weatherConfig.drawFunction) { - // Call the feature's weather analysis draw function - weatherConfig.drawFunction(); + if (isExpanded) { + if (weatherConfig.drawFunction) { + // Call the feature's weather analysis draw function + weatherConfig.drawFunction(); + } + ImGui::TreePop(); } ImGui::PopID(); @@ -1188,7 +1197,7 @@ void CSEditor::DrawOverlay() WeatherDetailsWindow.Enabled = true; } bool* p_open = &WeatherDetailsWindow.Enabled; - RenderWeatherDetailsWindow(p_open); + RenderWeatherDetailsWindow(p_open, false); } s_prevOverlayVisible = overlayVisible; } diff --git a/src/Features/CSEditor.h b/src/Features/CSEditor.h index 4db82f4694..554731c393 100644 --- a/src/Features/CSEditor.h +++ b/src/Features/CSEditor.h @@ -50,7 +50,7 @@ struct CSEditor : OverlayFeature * Renders the standalone weather details window. * @param open Pointer to the open/close state owned by the caller. */ - void RenderWeatherDetailsWindow(bool* open); + void RenderWeatherDetailsWindow(bool* open, bool showSectionHeaders = true); // Core weather display functions that other features can use /** @@ -64,7 +64,7 @@ struct CSEditor : OverlayFeature * Renders the core weather details UI section. * @param showInteractiveElements Enables interactive controls when true. */ - static void RenderCoreWeatherDetails(bool showInteractiveElements = true); + static void RenderCoreWeatherDetails(bool showInteractiveElements = true, bool showSectionHeaders = true); /** * Renders weather analysis sections contributed by other features. */ @@ -75,13 +75,13 @@ struct CSEditor : OverlayFeature * Renders the weather controls section. * @param sky Active sky instance. */ - static void RenderWeatherControls(RE::Sky* sky); + static void RenderWeatherControls(RE::Sky* sky, bool showSectionHeader = true); /** * Renders the weather information display section. * @param sky Active sky instance. * @param showInteractiveElements Enables interactive controls when true. */ - static void RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements = true); + static void RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements = true, bool showSectionHeader = true); struct WeatherDetailsWindowSettings { @@ -122,6 +122,7 @@ struct CSEditor : OverlayFeature static ImVec4 GetWeatherFlagColorByName(const std::string& flagName); private: + void DrawShowInOverlayToggle(); void DrawTimeControls(); void DrawWeatherStatusPanel(); void DrawWeatherPickerSection(); diff --git a/src/Features/WetnessEffects.cpp b/src/Features/WetnessEffects.cpp index 7cdca4fcfa..f634439a3e 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -233,6 +233,13 @@ static const char* GetClimatePresetShortDescription(size_t a_index) } } +static void DrawWeatherAnalysisLabel(const char* a_label) +{ + const auto& palette = Menu::GetSingleton()->GetTheme().Palette; + ImGui::TextColored(palette.Text, "%s", a_label); + ImGui::Spacing(); +} + static std::vector GetClimatePresetDetailedDescription(size_t a_index) { switch (a_index) { @@ -615,13 +622,12 @@ void WetnessEffects::DrawSettings() ImGui::Spacing(); auto& csEditor = globals::features::csEditor; if (csEditor.loaded) { - std::string csEditorName = csEditor.GetName(); - if (ImGui::SmallButton(std::vformat(T(TKEY("open_feature"), "Open {}"), std::make_format_args(csEditorName)).c_str())) { + if (ImGui::SmallButton(T(TKEY("open_weather_picker"), "Open Weather Picker"))) { // Navigate to the replacement feature in the menu Menu::GetSingleton()->SelectFeatureMenu(csEditor.GetShortName()); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text(T(TKEY("open_installed_feature_tooltip"), "Open the installed %s feature"), csEditorName.c_str()); + ImGui::Text("%s", T(TKEY("open_weather_picker_tooltip"), "Open the Weather Picker in CS Utility")); } } @@ -1001,14 +1007,11 @@ void WetnessEffects::DrawWeatherAnalysis() const if (weatherMaxParticleDensity <= 0.0f && sky->lastWeather && sky->lastWeather->precipitationData) { weatherMaxParticleDensity = sky->lastWeather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; } - // // Consolidated Shader & Weather Analysis - static bool rainAnalysisExpanded = true; - Util::DrawSectionHeader("Rain Analysis", false, true, &rainAnalysisExpanded); - - if (rainAnalysisExpanded) { + // Consolidated Shader & Weather Analysis + { // Climate Preset Information Section - auto climateSection = Util::SectionWrapper("Current Climate Preset"); - if (climateSection) { + DrawWeatherAnalysisLabel(T(TKEY("current_climate_preset"), "Current Climate Preset")); + { // const auto& climate = GetClimateSettings(climatePreset); // Unused, remove to fix warning treated as error const auto& presetInfo = CLIMATE_PRESET_INFO[static_cast(climatePreset)]; @@ -1037,8 +1040,9 @@ void WetnessEffects::DrawWeatherAnalysis() const ImGui::Text("Raindrop Chance: %.1f%% (preset value)", settings.RaindropChance * 100.0f); ImGui::Unindent(); } - auto section = Util::SectionWrapper("Rain System State"); - if (section && sky->currentWeather) { + ImGui::Spacing(); + DrawWeatherAnalysisLabel(T(TKEY("rain_system_state"), "Rain System State")); + if (sky->currentWeather) { float gridSizeGameUnits = 1.0f / frameData.settings.RaindropGridSize; float gridSizeMeters = Util::Units::GameUnitsToMeters(gridSizeGameUnits); float intervalSeconds = 1.0f / frameData.settings.RaindropInterval; @@ -1050,7 +1054,7 @@ void WetnessEffects::DrawWeatherAnalysis() const float theoreticalMaxRainRate = CalculatePrecipitationRate( presetSettings.raindropChance, presetSettings.raindropGridSize, presetSettings.raindropInterval); - if (ImGui::BeginTable("RainAnalysis", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) { + if (ImGui::BeginTable("RainAnalysis", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders)) { ImGui::TableSetupColumn("Current Shader State", ImGuiTableColumnFlags_WidthStretch, 0.5f); ImGui::TableSetupColumn("Precipitation Analysis", ImGuiTableColumnFlags_WidthStretch, 0.5f); ImGui::TableHeadersRow(); diff --git a/src/Menu.cpp b/src/Menu.cpp index 99e716d600..f9b3f44b03 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -1225,7 +1225,7 @@ void Menu::DrawWeatherDetailsWindow() // Use Weather core feature for all window management and rendering auto& weather = globals::features::csEditor; bool* p_open = &globals::features::csEditor.WeatherDetailsWindow.Enabled; - weather.RenderWeatherDetailsWindow(p_open); + weather.RenderWeatherDetailsWindow(p_open, !weather.WeatherDetailsWindow.ShowInOverlay); } /** diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index e1c0fabdef..f1483611d3 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -12,6 +12,7 @@ #include "Feature.h" #include "FeatureConstraints.h" #include "FeatureIssues.h" +#include "Features/CSEditor.h" #include "Fonts.h" #include "Globals.h" #include "I18n/I18n.h" @@ -811,8 +812,10 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImVec2 cursorPosBefore = ImGui::GetCursorPos(); feat->DrawSettings(); - ImGui::SeparatorText("Profiling"); - ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); + if (feat != &globals::features::csEditor) { + ImGui::SeparatorText("Profiling"); + ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); + } ImVec2 cursorPosAfter = ImGui::GetCursorPos(); From 18827187c58c8292099b0fbc6cfd791c2af9367c Mon Sep 17 00:00:00 2001 From: jiayev Date: Wed, 3 Jun 2026 04:40:26 +0800 Subject: [PATCH 16/55] feat(water-effects): water caustic with chromatic abberation (#2433) --- .../Shaders/Features/WaterEffects.ini | 2 +- .../Shaders/WaterEffects/WaterCaustics.hlsli | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/features/Water Effects/Shaders/Features/WaterEffects.ini b/features/Water Effects/Shaders/Features/WaterEffects.ini index f4fcedc7a2..7c4d5a2a34 100644 --- a/features/Water Effects/Shaders/Features/WaterEffects.ini +++ b/features/Water Effects/Shaders/Features/WaterEffects.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-1-3 +Version = 1-2-0 \ No newline at end of file diff --git a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli index 84e2155a8e..7a9d793453 100644 --- a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli +++ b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli @@ -12,7 +12,18 @@ namespace WaterEffects return WaterCaustics.Sample(SampColorSampler, uv).x; } - float ComputeCaustics(float4 waterData, float3 worldPosition, uint eyeIndex) + // Approximate wavelength-dependent refraction by offsetting red/blue around green. + float3 SampleCausticsDispersion(float2 uv, float2 dispersionOffset) + { + float center = SampleCaustics(uv); + float3 dispersed = float3( + SampleCaustics(uv - dispersionOffset * 0.75), + center, + SampleCaustics(uv + dispersionOffset)); + return lerp(center.xxx, dispersed, 0.5); + } + + float3 ComputeCaustics(float4 waterData, float3 worldPosition, uint eyeIndex) { float causticsDistToWater = waterData.w - worldPosition.z; float shoreFactorCaustics = saturate(causticsDistToWater / 64.0); @@ -22,29 +33,27 @@ namespace WaterEffects causticsFade *= causticsFade; float2 causticsUV = (worldPosition.xy + FrameBuffer::CameraPosAdjust[eyeIndex].xy) * 0.005; + float2 dispersionOffset = float2(0.6, 0.8) * (0.025 * shoreFactorCaustics * saturate(causticsDistToWater / 256.0)); float2 causticsUV1 = PanCausticsUV(causticsUV, 0.5 * 0.2, 1.0); float2 causticsUV2 = PanCausticsUV(causticsUV, 1.0 * 0.2, -0.5); - const float causticsHigh = - (causticsFade > 0.0) - ? (min(SampleCaustics(causticsUV1), SampleCaustics(causticsUV2)) * 4.0) - : 1.0; + const float3 causticsHigh = + (causticsFade > 0.0) ? (min(SampleCausticsDispersion(causticsUV1, dispersionOffset), SampleCausticsDispersion(causticsUV2, dispersionOffset)) * 4.0) : 1.0.xxx; causticsUV *= 0.5; + dispersionOffset *= 0.5; causticsUV1 = PanCausticsUV(causticsUV, 0.5 * 0.1, 1.0); causticsUV2 = PanCausticsUV(causticsUV, 1.0 * 0.1, -0.5); - const float causticsLow = - (causticsFade < 1.0) - ? (min(SampleCaustics(causticsUV1), SampleCaustics(causticsUV2)) * 4.0) - : 1.0; + const float3 causticsLow = + (causticsFade < 1.0) ? (min(SampleCausticsDispersion(causticsUV1, dispersionOffset), SampleCausticsDispersion(causticsUV2, dispersionOffset)) * 4.0) : 1.0.xxx; - const float caustics = lerp(causticsLow, causticsHigh, causticsFade); - return lerp(1.0, caustics, shoreFactorCaustics); + const float3 caustics = lerp(causticsLow, causticsHigh, causticsFade); + return lerp(1.0.xxx, caustics, shoreFactorCaustics); } - return 1.0; + return 1.0.xxx; } } From 6e82454bfb1d9ad5bba805e85c4512aa640f897d Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:40:49 +0200 Subject: [PATCH 17/55] ci: reconcile dev by merge instead of rebase (#2453) --- .claude/CLAUDE.md | 14 +- .github/workflows/release-semantic.yaml | 246 ++++++++++++++++-------- 2 files changed, 168 insertions(+), 92 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index cb0ac9c674..7acc140a98 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -463,12 +463,12 @@ Conventional commits drive semantic-release. `feat:` triggers a minor bump, `fix **Default branch for PRs is `dev`.** Feature work, fixes, and refactors all land there via normal PRs. `main` is updated only through the release workflows — never PR a feature branch directly into `main`. -**Branch lineage invariant:** after every release reconciles, `main` is an ancestor of `dev`, so every tag on `main` is reachable from `dev`. The `Release: Semantic Version` workflow keeps this invariant in two ways depending on the promotion source: +**Branch lineage invariant:** `main` becomes an ancestor of `dev` at **each minor/major promotion** (every tag on `main` is then reachable from `dev`). Current-line hotfixes intentionally let `main` diverge from `dev` until the next promotion folds them back in. `dev` is **never rewritten** — the `Release: Semantic Version` workflow reconciles per promotion source: -- **dev → main promotion** (minor/major): main fast-forwards to the dev SHA, semantic-release appends a `chore(release):` commit on top, then dev fast-forwards to absorb that commit. No history rewrites on either branch. -- **hotfix-staging → main promotion** (current-line patch): main fast-forwards to the hotfix-staging SHA, semantic-release appends the `chore(release):` commit, then dev is **rebase-reconciled** onto the new main. `git rebase` drops dev's originals of the cherry-picked fixes (patch-id match) and replays any unique dev work on top. This is the only place the workflow force-pushes (`--force-with-lease`) — it is intentional and load-bearing. +- **dev → main promotion** (minor/major): if interim hotfixes have diverged `main`, the workflow first **merges `main` into `dev`** (a single ancestry-only merge commit; the merge tree equals `dev`'s, with version-bump files resolved to `dev`, and any non-`dev`-sourced divergence hard-fails before pushing). That merge is a fast-forward push of `dev` (**no force** — the App's PR-bypass authorizes it). Then `main` FFs to the merge commit, semantic-release appends `chore(release):`, and `dev` FFs to absorb it. A best-effort step dedups the new release's notes of the carried-over hotfix entries. +- **hotfix-staging → main promotion** (current-line patch): `main` fast-forwards to the hotfix-staging SHA and semantic-release appends `chore(release):`. **`dev` is not touched** — it is reconciled at the next minor/major promotion via the merge above. No rebase, no force-push of `dev`. -After a hotfix release, open PRs targeting `dev` are auto-rebased by the `Auto-rebase open PRs` workflow (a thin wrapper around `peter-evans/rebase@v3`). PRs from forks need "Allow edits by maintainers" enabled or the action silently skips them; drafts and PRs labeled `no-auto-rebase` are also excluded. The workflow's job summary reports the rebased count and lists the buckets PRs can fall into; conflict-skipped PRs need a manual `git rebase origin/dev` by the author. +**Prerequisite:** the release App (`community-shaders-release-bot`) must be in the **"Allow specified actors to bypass required pull requests"** list for **both `main` and `dev`** — the app token alone cannot bypass the PR requirement, so a missing entry fails the FF push with `GH006: Changes must be made through a pull request`. **Patch flow (current line _or_ older line, same staging mechanism):** @@ -477,17 +477,17 @@ After a hotfix release, open PRs targeting `dev` are auto-rebased by the `Auto-r 3. PR checks build a `vX.Y.Z-prNNNN` prerelease for verification. 4. Merge the candidate PR. 5. Cut the release: - - **Current line** (`main` is on `X.Y`): dispatch **Release: Semantic Version** on `main` with `ff_target = `. + - **Current line** (`main` is on `X.Y`): dispatch **Release: Semantic Version** on `main` with `ff_target = ` — **not** the `hotfix/X.Y.x` tip, which is a merge commit that `main`'s branch protection rejects. Use the second parent of the merge commit: `git rev-parse origin/hotfix/X.Y.x^2`. `dev` is left untouched and is reconciled at the next minor/major promotion. - **Older line** (`main` has shipped a newer minor/major): dispatch **Release: Semantic Version** on `hotfix/X.Y.x` with `ff_target` empty. **Minor/major release flow:** 1. Cut RCs from `dev`: dispatch **Release: Semantic Version** on `dev`, `ff_target` empty → `vX.Y.Z-rc.N`. -2. When ready, dispatch **Release: Semantic Version** on `main` with `ff_target = ` (typically the latest RC's SHA). The workflow FFs `main`, runs semantic-release to cut stable, then FFs `dev` to absorb the `chore(release):` commit. +2. When ready, dispatch **Release: Semantic Version** on `main` with `ff_target = ` (typically the latest RC's SHA). If interim hotfixes have diverged `main`, the workflow first merges `main` into `dev` (ancestry-only, no force) and retargets to that merge commit; it then FFs `main`, runs semantic-release to cut stable, dedups the notes, and FFs `dev` to absorb the `chore(release):` commit. **Things agents should not do without explicit user direction:** -- Force-push or rebase `main`, `dev`, or any `hotfix/*` branch. (The release workflow's rebase-reconcile of `dev` after a hotfix-staging promotion is the one sanctioned exception; humans should not replicate it manually unless the workflow's remediation block explicitly instructs them to.) +- Force-push or rebase `main`, `dev`, or any `hotfix/*` branch. (The release workflow reconciles `dev` only via fast-forward and ancestry-only merge commits — it never rewrites `dev`.) - Manually create tags matching `v*` (semantic-release owns these). - Bump `CMakeLists.txt`'s `VERSION` field outside the release workflow. - PR a feature branch directly into `main`. diff --git a/.github/workflows/release-semantic.yaml b/.github/workflows/release-semantic.yaml index 9911c4159a..fac1c615dc 100644 --- a/.github/workflows/release-semantic.yaml +++ b/.github/workflows/release-semantic.yaml @@ -95,24 +95,33 @@ jobs: echo "::error::ff_target SHA ${FF_TARGET} does not exist." exit 1 fi - if ! git merge-base --is-ancestor origin/main "${FF_TARGET}"; then - echo "::error::main is not an ancestor of ff_target ${FF_TARGET} — fast-forward is not possible." - exit 1 - fi + # main being an ancestor of ff_target is enforced per-source + # below. dev-source promotions may legitimately have a + # diverged main (interim hotfixes); the reconcile pre-step + # merges main into dev to fix that before the FF. + # # Identify the source branch for the promotion. - # dev source → FF-reconcile dev afterward (no rewrite). - # hotfix-staging → rebase-reconcile dev afterward so the - # release commit + tag enter dev's - # ancestry. Force-with-lease push; `git - # rebase` drops patch-id duplicates of - # the cherry-picks already on main. + # dev source → merge main into dev first (if diverged), + # then FF-reconcile dev afterward. No rewrite. + # hotfix-staging → dev is NOT reconciled here; the next + # minor/major promotion folds the hotfix in + # via its dev-source merge pre-step. SOURCE="" - if git merge-base --is-ancestor "${FF_TARGET}" origin/dev; then + FF_RESOLVED="$(git rev-parse "${FF_TARGET}^{commit}")" + DEV_TIP="$(git rev-parse origin/dev^{commit})" + if [[ "${FF_RESOLVED}" == "${DEV_TIP}" ]]; then + # dev-source promotion: ff_target must be the CURRENT dev tip + # exactly. A stale (ancestor-but-not-tip) dev SHA would make + # the reconcile merge's first parent an old commit, and the + # force-with-lease push would then rewrite newer dev work. SOURCE="dev" + elif git merge-base --is-ancestor "${FF_RESOLVED}" origin/dev; then + echo "::error::ff_target ${FF_TARGET} is an ancestor of dev but not its tip ($(git rev-parse --short=9 origin/dev)). Promote the current dev tip (or cut a fresh RC) — promoting a stale dev point would rewrite newer dev commits." + exit 1 else while read -r REF; do [[ -z "${REF}" ]] && continue - if git merge-base --is-ancestor "${FF_TARGET}" "${REF}"; then + if git merge-base --is-ancestor "${FF_RESOLVED}" "${REF}"; then SOURCE="${REF#origin/}" break fi @@ -122,6 +131,15 @@ jobs: echo "::error::ff_target ${FF_TARGET} is not an ancestor of dev or any origin/hotfix/* branch — refusing to promote an unreviewed SHA." exit 1 fi + # hotfix-staging sources must be a clean fast-forward of main + # (no reconcile pre-step runs for them). dev sources may have a + # diverged main; the reconcile step merges it in first. + if [[ "${SOURCE}" != "dev" ]]; then + if ! git merge-base --is-ancestor origin/main "${FF_TARGET}"; then + echo "::error::hotfix-staging ff_target ${FF_TARGET} is not a fast-forward of main." + exit 1 + fi + fi echo "source=${SOURCE}" >> "$GITHUB_OUTPUT" { echo "## Promotion plan" @@ -135,11 +153,95 @@ jobs: git log --oneline "origin/main..${FF_TARGET}" | sed 's/^/ /' } >> "$GITHUB_STEP_SUMMARY" + - name: Reconcile dev (merge main into dev, dev source only) + id: reconcile + if: ${{ inputs.ff_target != '' && steps.validate.outputs.source == 'dev' }} + env: + TOKEN: ${{ steps.app-token.outputs.token }} + FF_TARGET: ${{ inputs.ff_target }} + run: | + set -euo pipefail + git fetch origin main dev + MAIN_BEFORE="$(git rev-parse origin/main)" + DEV_SHA="$(git rev-parse "${FF_TARGET}")" + echo "main_before=${MAIN_BEFORE}" >> "$GITHUB_OUTPUT" + echo "dev_before=${DEV_SHA}" >> "$GITHUB_OUTPUT" + + # dev is the sole source of truth. Version-bump files are the + # only paths allowed to diverge between main and dev (resolved + # to dev); anything else is a process violation and hard-fails. + ALLOWLIST_REGEX='^(CMakeLists\.txt|features/.+/Shaders/Features/[^/]+\.ini)$' + + if git merge-base --is-ancestor "${MAIN_BEFORE}" "${DEV_SHA}"; then + # main already folded into dev — nothing to merge. + echo "effective_target=${DEV_SHA}" >> "$GITHUB_OUTPUT" + echo "::notice::main already an ancestor of dev; no merge needed." + exit 0 + fi + + # Merge main into dev to establish ANCESTRY ONLY, on a detached + # HEAD so no local branch is mutated. The result tree must equal + # dev's tree (the merge adds history, not content). + git checkout --quiet --detach "${DEV_SHA}" + if git merge --no-ff --no-edit \ + -m "chore: merge main into dev (reconcile hotfixes) [skip ci]" "${MAIN_BEFORE}"; then + mapfile -t CHANGED < <(git diff --name-only "${DEV_SHA}" HEAD) + else + mapfile -t CHANGED < <(git diff --name-only --diff-filter=U) + MERGE_CONFLICTED=1 + fi + + # Any diverging path outside the allowlist is a tripwire: main + # must not carry non-dev-sourced content. Fail before any push. + VIOLATIONS=() + for p in "${CHANGED[@]}"; do + [[ -z "$p" ]] && continue + [[ "$p" =~ ${ALLOWLIST_REGEX} ]] || VIOLATIONS+=("$p") + done + if (( ${#VIOLATIONS[@]} > 0 )); then + echo "::error::main carries non-dev-sourced changes outside the version-file allowlist:" + printf ' %s\n' "${VIOLATIONS[@]}" + git merge --abort 2>/dev/null || true + exit 1 + fi + + # Resolve every diverging (allowlisted) path to dev's content. + for p in "${CHANGED[@]}"; do + [[ -z "$p" ]] && continue + git checkout "${DEV_SHA}" -- "$p" + git add -- "$p" + done + if [[ "${MERGE_CONFLICTED:-0}" -eq 1 ]]; then + git -c core.editor=true commit --no-edit >/dev/null + else + git diff --cached --quiet || git commit --amend --no-edit >/dev/null + fi + + # Belt-and-suspenders: merge tree MUST equal dev's tree. + if ! git diff --quiet "${DEV_SHA}" HEAD; then + echo "::error::post-resolution tree differs from dev; refusing to proceed" + git diff --name-only "${DEV_SHA}" HEAD + exit 1 + fi + + TARGET="$(git rev-parse HEAD)" + echo "effective_target=${TARGET}" >> "$GITHUB_OUTPUT" + + # Fast-forward push of dev to the merge commit (its first parent + # is the old dev tip) — no force, so dev's protection (force-push + # disabled) is respected; the App's PR-bypass authorizes it. + REMOTE="https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" + CURRENT_DEV="$(git rev-parse origin/dev)" + git push --force-with-lease="dev:${CURRENT_DEV}" "${REMOTE}" "${TARGET}:refs/heads/dev" + echo "::notice::Merged main into dev (${TARGET}); dev fast-forwarded." + - name: Fast-forward main to ff_target if: ${{ inputs.ff_target != '' }} env: TOKEN: ${{ steps.app-token.outputs.token }} - FF_TARGET: ${{ inputs.ff_target }} + # Use the reconcile merge commit when dev was reconciled, + # otherwise the originally dispatched ff_target. + FF_TARGET: ${{ steps.reconcile.outputs.effective_target || inputs.ff_target }} run: | set -euo pipefail REMOTE="https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" @@ -194,6 +296,50 @@ jobs: --repo "${{ github.repository }}" \ --ref "${TAG}" + - name: Dedup release notes (drop interim hotfix entries) + # Best-effort: prune fixes already shipped via interim hotfixes + # from this minor/major's draft notes. semantic-release created the + # draft (with notes) in the step above; release-build.yaml only + # updates it minutes later after its build, so this quick edit lands + # first. continue-on-error so notes cosmetics never block a release. + if: ${{ inputs.ff_target != '' && steps.validate.outputs.source == 'dev' && steps.semantic.outputs.new_release_published == 'true' }} + continue-on-error: true + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_REPO: ${{ github.repository }} + MAIN_BEFORE: ${{ steps.reconcile.outputs.main_before }} + DEV_BEFORE: ${{ steps.reconcile.outputs.dev_before }} + TAG: ${{ steps.semantic.outputs.new_release_git_tag }} + run: | + set -euo pipefail + git fetch origin --tags --force + # Interim hotfix commits live between the previous minor (the + # merge-base of the pre-promotion main and dev) and the + # pre-promotion main tip. Collect their PR numbers and the + # `cherry picked from commit ` trailers as dedup keys. + BASE="$(git merge-base "${MAIN_BEFORE}" "${DEV_BEFORE}")" + KEYS="$(mktemp)" + { + git log "${BASE}..${MAIN_BEFORE}" --format='%s' | grep -oE '#[0-9]+' || true + git log "${BASE}..${MAIN_BEFORE}" --format='%b' \ + | grep -oiE 'cherry picked from commit [0-9a-f]{7,40}' \ + | awk '{print substr($NF,1,9)}' || true + } | sort -u > "${KEYS}" + if [[ ! -s "${KEYS}" ]]; then + echo "no interim hotfix keys; nothing to dedup"; exit 0 + fi + # semantic-release already created the draft with notes; drop + # any line referencing an already-shipped key, then update it. + BODY="$(mktemp)" + gh release view "${TAG}" --json body -q .body > "${BODY}" 2>/dev/null || true + grep -vF -f "${KEYS}" "${BODY}" > "${BODY}.new" || true + if ! diff -q "${BODY}" "${BODY}.new" >/dev/null; then + gh release edit "${TAG}" --notes-file "${BODY}.new" + echo "deduped release notes for ${TAG}" + else + echo "no shipped keys found in ${TAG} notes" + fi + - name: Reconcile dev with release commit (promotion mode, dev source only) if: ${{ inputs.ff_target != '' && steps.semantic.outputs.new_release_published == 'true' && steps.validate.outputs.source == 'dev' }} env: @@ -214,8 +360,8 @@ jobs: echo "git fetch && git checkout dev && git merge --ff-only origin/main && git push" echo '```' } >> "$GITHUB_STEP_SUMMARY" - # Fail loud: tag is on main but dev isn't reconciled - # (same invariant break as the hotfix-staging path). + # Fail loud: tag is on main but dev isn't reconciled, so the + # next RC would compute its floor against the wrong prior tag. echo "::error::dev moved during promotion — dev is unreconciled. See job summary for manual remediation." exit 1 fi @@ -224,73 +370,3 @@ jobs: CURRENT_DEV=$(git rev-parse origin/dev) git push --force-with-lease="dev:${CURRENT_DEV}" "${REMOTE}" "${NEW_MAIN}:refs/heads/dev" echo "::notice::Reconciled dev to main ($NEW_MAIN)." - - - name: Rebase-reconcile dev with release commit (promotion mode, hotfix-staging source) - if: ${{ inputs.ff_target != '' && steps.semantic.outputs.new_release_published == 'true' && steps.validate.outputs.source != 'dev' && steps.validate.outputs.source != '' }} - env: - TOKEN: ${{ steps.app-token.outputs.token }} - run: | - set -euo pipefail - git fetch origin main dev - NEW_MAIN=$(git rev-parse origin/main) - CURRENT_DEV=$(git rev-parse origin/dev) - - # FF-only won't work: dev and main diverged. Rebase - # preserves linear history; patch-id dedup drops the - # cherry-picked-fix commits automatically. - git checkout -B dev "${CURRENT_DEV}" - if ! git rebase "${NEW_MAIN}"; then - git rebase --abort || true - { - echo "" - echo "## ⚠️ Dev reconcile FAILED — manual intervention required" - echo "" - echo "Rebasing \`dev\` onto the new \`main\` (\`$(git rev-parse --short=9 "${NEW_MAIN}")\`) hit a conflict." - echo "The hotfix release is live on \`main\`, but \`dev\` has NOT absorbed it." - echo "Until reconciled, the next RC cut on \`dev\` will compute the wrong version floor." - echo "" - echo "**Recover by running locally:**" - echo "" - echo '```bash' - echo "git fetch origin" - echo "git checkout dev && git reset --hard origin/dev" - echo "git rebase origin/main # resolve conflicts, git rebase --continue" - echo "git push --force-with-lease origin dev" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - echo "::error::Dev rebase-reconcile failed — see job summary for remediation." - exit 1 - fi - - REMOTE="https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" - # --force-with-lease guards against a concurrent push to - # dev: if someone landed a commit during the release, - # abort instead of overwriting their work. - if ! git push --force-with-lease="dev:${CURRENT_DEV}" "${REMOTE}" "HEAD:refs/heads/dev"; then - { - echo "" - echo "## ⚠️ Dev push rejected (lease check failed)" - echo "" - echo "Dev was pushed to during the release. Reconcile manually:" - echo "" - echo '```bash' - echo "git fetch origin && git checkout dev && git reset --hard origin/dev" - echo "git rebase origin/main && git push --force-with-lease origin dev" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - # Fail loud: tag is on main but dev's history doesn't - # contain it, so the next RC would compute its floor - # and changelog against the wrong prior tag. - echo "::error::Dev moved during release — dev is unreconciled. See job summary for manual remediation." - exit 1 - fi - - { - echo "" - echo "## Dev reconciled (rebase-forward)" - echo "- Source: hotfix-staging \`${{ steps.validate.outputs.source }}\`" - echo "- New main: \`$(git rev-parse --short=9 "${NEW_MAIN}")\`" - echo "- Dev rebased linearly onto new main; identical-patch commits auto-dropped by \`git rebase\`." - echo "- Open PRs against dev will need a manual rebase by their authors." - } >> "$GITHUB_STEP_SUMMARY" - echo "::notice::Reconciled dev onto main via rebase (linear history preserved)." From be22b78909ecbb387109bf3b57705a91bfedf85c Mon Sep 17 00:00:00 2001 From: jiayev Date: Wed, 3 Jun 2026 17:07:07 +0800 Subject: [PATCH 18/55] feat(fog): volumetric fog (#2361) --- .../ExponentialHeightFog.hlsli | 240 ++++++- .../VolumetricFogCSCommon.hlsli | 58 ++ .../VolumetricFogCommon.hlsli | 101 +++ .../VolumetricFogConservativeDepthCS.hlsl | 34 + .../VolumetricFogIntegrationCS.hlsl | 42 ++ .../VolumetricFogLightScatteringCS.hlsl | 429 +++++++++++++ .../VolumetricFogMaterialCS.hlsl | 18 + .../Shaders/Features/ExponentialHeightFog.ini | 2 +- .../CommunityShaders/Translations/en.json | 23 + .../CommunityShaders/Translations/zh_CN.json | 25 +- package/Shaders/Common/SharedData.hlsli | 21 +- package/Shaders/DistantTree.hlsl | 24 + package/Shaders/Effect.hlsl | 7 +- package/Shaders/ISSAOComposite.hlsl | 3 +- package/Shaders/Lighting.hlsl | 9 +- package/Shaders/Sky.hlsl | 11 + package/Shaders/Water.hlsl | 4 +- src/Features/ExponentialHeightFog.cpp | 605 +++++++++++++++++- src/Features/ExponentialHeightFog.h | 83 ++- src/State.cpp | 3 + 20 files changed, 1706 insertions(+), 36 deletions(-) create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCommon.hlsli create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl create mode 100644 features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index c2e6bba23f..c15a0280d3 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -1,12 +1,16 @@ #ifndef __EXPONENTIAL_HEIGHT_FOG_HLSLI__ #define __EXPONENTIAL_HEIGHT_FOG_HLSLI__ +#include "Common/Random.hlsli" #include "Common/SharedData.hlsli" +#include "ExponentialHeightFog/VolumetricFogCommon.hlsli" #if defined(DYNAMIC_CUBEMAPS) # include "DynamicCubemaps/DynamicCubemaps.hlsli" #endif +Texture3D ExponentialHeightFogIntegratedLightScattering : register(t19); + namespace ExponentialHeightFog { float GetVanillaFogFade(float vanillaFogFade) @@ -19,32 +23,213 @@ namespace ExponentialHeightFog return SharedData::exponentialHeightFogSettings.enabled && SharedData::exponentialHeightFogSettings.disableVanillaFog != 0; } - // Henyey-Greenstein phase function for physically-based inscattering. - // g: asymmetry parameter [-1, 1]. Positive = forward scattering, 0 = isotropic. - float HenyeyGreenstein(float cosTheta, float g) + uint GetEyeIndexFromCameraWS(float3 cameraWS) { - float g2 = g * g; - float denom = 1.0f + g2 - 2.0f * g * cosTheta; - return (1.0f - g2) / (4.0f * Math::PI * pow(max(denom, 1e-5f), 1.5f)); +#if defined(VR) + return distance(cameraWS, FrameBuffer::CameraPosAdjust[1].xyz) < distance(cameraWS, FrameBuffer::CameraPosAdjust[0].xyz) ? 1u : 0u; +#else + return 0u; +#endif } - float4 GetExponentialHeightFog(float3 positionWS, float3 cameraWS, float3 fogColor) + bool ShouldApplyVolumetricFog() + { + return SharedData::exponentialHeightFogSettings.enabled != 0 && + SharedData::exponentialHeightFogSettings.volumetricFogEnabled != 0 && + SharedData::exponentialHeightFogSettings.volumetricFogDistance > SharedData::exponentialHeightFogSettings.volumetricFogStartDistance + 1.0f; + } + + float GetSceneDepthFromClip(float4 clipPosition) + { + return max(clipPosition.w, SharedData::CameraData.y); + } + + float GetSceneDepthForFog(float3 positionWS, uint eyeIndex, out float2 volumeUV, out float projectedDepth) + { + float4 clipPosition = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1.0f)); + [branch] if (clipPosition.w <= 0.0f) + { + volumeUV = 0.0f.xx; + projectedDepth = 0.0f; + return 0.0f; + } + + projectedDepth = GetSceneDepthFromClip(clipPosition); + volumeUV = clipPosition.xy / clipPosition.w * float2(0.5f, -0.5f) + 0.5f; + + volumeUV = saturate(volumeUV); + return projectedDepth; + } + + float4 SampleVolumetricFog(float3 positionWS, uint eyeIndex) + { + if (!ShouldApplyVolumetricFog()) + return float4(0.0f, 0.0f, 0.0f, 1.0f); + + uint volumeWidth; + uint volumeHeight; + uint volumeDepth; + ExponentialHeightFogIntegratedLightScattering.GetDimensions(volumeWidth, volumeHeight, volumeDepth); + if (volumeWidth == 0 || volumeHeight == 0 || volumeDepth == 0) + return float4(0.0f, 0.0f, 0.0f, 1.0f); + + float2 volumeUV; + float projectedDepth; + float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); + if (projectedDepth <= 0.0f) + return float4(0.0f, 0.0f, 0.0f, 1.0f); + +#if defined(VR) + volumeUV = Stereo::ConvertToStereoUV(volumeUV, eyeIndex); +#endif + + float volumeZ = saturate(ComputeVolumetricNormalizedSlice(sceneDepth, float(volumeDepth))); + + float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); + float2 volumeUVMin = volumeTexelCenter.xy; + float2 volumeUVMax = 1.0f.xx - volumeTexelCenter.xy; +#if defined(VR) + float eyeMinX = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; + float eyeMaxX = (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x; + volumeUVMin.x = eyeMinX; + volumeUVMax.x = eyeMaxX; +#endif + float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); + float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); + return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); + } + + float2 GetVolumetricFogUVMax(float2 volumeSize, float gridPixelSize) + { + float2 physicalSize = max(volumeSize * gridPixelSize, 1.0f.xx); + float2 viewSizeSafe = ceil(SharedData::BufferDim.xy / gridPixelSize) * gridPixelSize - (gridPixelSize * 0.5f + 1.0f); + return saturate(viewSizeSafe / physicalSize); + } + + float4 SampleVolumetricFog(float4 screenPosition, uint eyeIndex) + { + if (!ShouldApplyVolumetricFog()) + return float4(0.0f, 0.0f, 0.0f, 1.0f); + + uint volumeWidth; + uint volumeHeight; + uint volumeDepth; + ExponentialHeightFogIntegratedLightScattering.GetDimensions(volumeWidth, volumeHeight, volumeDepth); + if (volumeWidth == 0 || volumeHeight == 0 || volumeDepth == 0) + return float4(0.0f, 0.0f, 0.0f, 1.0f); + + float sceneDepth = SharedData::GetScreenDepth(screenPosition.z); + float volumeZ = saturate(ComputeVolumetricNormalizedSlice(sceneDepth, float(volumeDepth))); + + float2 volumeSize = float2(volumeWidth, volumeHeight); + float2 inferredGridPixelSize = ceil(SharedData::BufferDim.xy / max(volumeSize, 1.0f.xx)); + float gridPixelSize = max(max(inferredGridPixelSize.x, inferredGridPixelSize.y), 1.0f); + float2 jitter = 0.0f.xx; + [branch] if (SharedData::exponentialHeightFogSettings.volumetricUpsampleJitterMultiplier > 0.0f) + { + float2 noise = float2( + Random::InterleavedGradientNoise(screenPosition.xy, SharedData::FrameCount), + Random::InterleavedGradientNoise(screenPosition.yx + 19.19f, SharedData::FrameCount)); + jitter = (noise * 2.0f - 1.0f) * SharedData::exponentialHeightFogSettings.volumetricUpsampleJitterMultiplier * gridPixelSize; + } + + float2 volumeUV = (screenPosition.xy + jitter) / (volumeSize * gridPixelSize); + float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); + float2 volumeUVMin = volumeTexelCenter.xy; + float2 volumeUVMax = max(GetVolumetricFogUVMax(volumeSize, gridPixelSize), volumeUVMin); +#if defined(VR) + volumeUVMin.x = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; + volumeUVMax.x = max(volumeUVMin.x, min(volumeUVMax.x, (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x)); +#endif + float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); + float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); + return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); + } + + // Apply per-pixel directional light phase correction to volumetric fog. + // The volumetric compute stores directional scattering with isotropic phase (1/4PI) to + // avoid angular aliasing at coarse froxel XY resolution. Here we restore the correct + // per-pixel HG phase, weighted by the estimated directional light fraction. + float4 ApplyDirectionalPhaseCorrection(float4 volumetricFog, float3 viewDirection) + { + if (volumetricFog.r + volumetricFog.g + volumetricFog.b < 1e-7f) + return volumetricFog; + + float g = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; + float cosTheta = dot(normalize(SharedData::DirLightDirection.xyz), viewDirection); + float perPixelPhase = HenyeyGreenstein(cosTheta, g); + float isotropicPhase = 1.0f / (4.0f * Math::PI); + + // Estimate directional light's fraction of total volumetric inscattering + float dirStrength = dot(SharedData::DirLightColor.xyz, float3(0.2126f, 0.7152f, 0.0722f)) * + SharedData::exponentialHeightFogSettings.volumetricDirectionalScatteringIntensity; + float skyStrength = SharedData::exponentialHeightFogSettings.volumetricSkyLightingIntensity; + float dirFraction = saturate(dirStrength / max(dirStrength + skyStrength, 1e-5f)); + + // Apply phase correction only to the estimated directional portion + float correction = lerp(1.0f, perPixelPhase / isotropicPhase, dirFraction); + volumetricFog.rgb *= correction; + return volumetricFog; + } + + float4 CombineVolumetricFog(float4 analyticalFog, float3 positionWS, uint eyeIndex, float3 viewDirection) + { + float4 volumetricFog = SampleVolumetricFog(positionWS, eyeIndex); + volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); + float analyticalTransmittance = 1.0f - analyticalFog.w; + float combinedTransmittance = volumetricFog.a * analyticalTransmittance; + float combinedOpacity = saturate(1.0f - combinedTransmittance); + float3 analyticalPremultiplied = analyticalFog.rgb * analyticalFog.w; + float3 combinedPremultiplied = volumetricFog.rgb + volumetricFog.a * analyticalPremultiplied; + return float4(combinedOpacity > 1e-4f ? combinedPremultiplied / combinedOpacity : float3(0.0f, 0.0f, 0.0f), combinedOpacity); + } + + float4 CombineVolumetricFog(float4 analyticalFog, float4 screenPosition, uint eyeIndex, float3 viewDirection) + { + float4 volumetricFog = SampleVolumetricFog(screenPosition, eyeIndex); + volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); + float analyticalTransmittance = 1.0f - analyticalFog.w; + float combinedTransmittance = volumetricFog.a * analyticalTransmittance; + float combinedOpacity = saturate(1.0f - combinedTransmittance); + float3 analyticalPremultiplied = analyticalFog.rgb * analyticalFog.w; + float3 combinedPremultiplied = volumetricFog.rgb + volumetricFog.a * analyticalPremultiplied; + return float4(combinedOpacity > 1e-4f ? combinedPremultiplied / combinedOpacity : float3(0.0f, 0.0f, 0.0f), combinedOpacity); + } + + float4 GetExponentialHeightFogInternal(float3 positionWS, float3 cameraWS, float3 fogColor, bool useScreenPosition, float4 screenPosition, bool applyVolumetricFog) { float fogHeightFalloff = SharedData::exponentialHeightFogSettings.fogHeightFalloff * 0.001f; float fogDensity = SharedData::exponentialHeightFogSettings.fogDensity * 0.001f; if (fogDensity <= 0.0f) { return 0.0f; } + uint eyeIndex = GetEyeIndexFromCameraWS(cameraWS); float3 viewToPos = positionWS; + float2 volumeUV; + float projectedDepth; + float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); + [branch] if (projectedDepth > 1e-4f && sceneDepth > projectedDepth) + { + viewToPos *= sceneDepth / projectedDepth; + } + float viewToPosLength = length(viewToPos); - float viewToPosLengthInv = rcp(viewToPosLength); + float viewToPosLengthInv = rcp(max(viewToPosLength, 1e-4f)); float rayOriginTerms = fogDensity * exp2(-fogHeightFalloff * max(cameraWS.z - SharedData::exponentialHeightFogSettings.fogHeight, 0)); float rayLength = viewToPosLength; float rayDirectionZ = viewToPos.z; - if (SharedData::exponentialHeightFogSettings.startDistance > 0) { - float excludeIntersectionTime = SharedData::exponentialHeightFogSettings.startDistance * viewToPosLengthInv; + float excludeDistance = SharedData::exponentialHeightFogSettings.startDistance; + if (applyVolumetricFog && ShouldApplyVolumetricFog()) { + float cosAngle = sceneDepth * viewToPosLengthInv; + float invCosAngle = cosAngle > 0.001f ? rcp(cosAngle) : 0.0f; + excludeDistance = max(excludeDistance, GetVolumetricEndDistance() * invCosAngle); + } + + if (excludeDistance > 0) { + excludeDistance = min(excludeDistance, viewToPosLength); + float excludeIntersectionTime = excludeDistance * viewToPosLengthInv; float cameraToExclusionIntersectionZ = excludeIntersectionTime * viewToPos.z; float exclusionIntersectionZ = cameraWS.z + cameraToExclusionIntersectionZ; rayLength = (1.0f - excludeIntersectionTime) * viewToPosLength; @@ -75,18 +260,43 @@ namespace ExponentialHeightFog float3 directionalInscattering = 0; + float3 viewDirection = viewToPos * viewToPosLengthInv; + // Calculate directional light inscattering using Henyey-Greenstein phase function if (SharedData::exponentialHeightFogSettings.directionalInscatteringMultiplier > 0) { - float cosTheta = dot(normalize(positionWS), SharedData::DirLightDirection.xyz); + float3 lightDirection = normalize(SharedData::DirLightDirection.xyz); + float cosTheta = dot(lightDirection, viewDirection); float phase = HenyeyGreenstein(cosTheta, SharedData::exponentialHeightFogSettings.directionalInscatteringAnisotropy); float3 directionalLightInscattering = SharedData::DirLightColor.xyz * phase; - float dirExponentialHeightLineIntegral = exponentialHeightLineIntegralCalc * max(rayLength - SharedData::exponentialHeightFogSettings.startDistance, 0); - float dirExpFogFactor = saturate(exp2(-dirExponentialHeightLineIntegral)); - directionalInscattering = directionalLightInscattering * (1 - dirExpFogFactor) * SharedData::exponentialHeightFogSettings.directionalInscatteringMultiplier; + directionalInscattering = directionalLightInscattering * (1.0f - expFogFactor) * SharedData::exponentialHeightFogSettings.directionalInscatteringMultiplier; } fogColor += directionalInscattering; - return float4(fogColor, 1.0f - expFogFactor); + float4 analyticalFog = float4(fogColor, 1.0f - expFogFactor); + if (!applyVolumetricFog) { + return analyticalFog; + } + return useScreenPosition ? CombineVolumetricFog(analyticalFog, screenPosition, eyeIndex, viewDirection) : CombineVolumetricFog(analyticalFog, positionWS, eyeIndex, viewDirection); + } + + float4 GetExponentialHeightFog(float3 positionWS, float3 cameraWS, float3 fogColor) + { + return GetExponentialHeightFogInternal(positionWS, cameraWS, fogColor, false, 0.0f.xxxx, true); + } + + float4 GetExponentialHeightFog(float3 positionWS, float3 cameraWS, float3 fogColor, float4 screenPosition) + { + return GetExponentialHeightFogInternal(positionWS, cameraWS, fogColor, true, screenPosition, true); + } + + float4 GetExponentialHeightFogNoVolumetric(float3 positionWS, float3 cameraWS, float3 fogColor) + { + return GetExponentialHeightFogInternal(positionWS, cameraWS, fogColor, false, 0.0f.xxxx, false); + } + + float4 GetExponentialHeightFogNoVolumetric(float3 positionWS, float3 cameraWS, float3 fogColor, float4 screenPosition) + { + return GetExponentialHeightFogInternal(positionWS, cameraWS, fogColor, true, screenPosition, false); } float GetSunlightFogAttenuation(float3 positionWS, float3 cameraWS) diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli new file mode 100644 index 0000000000..bdd69fb861 --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli @@ -0,0 +1,58 @@ +#ifndef __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_CS_COMMON_HLSLI__ +#define __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_CS_COMMON_HLSLI__ + +#include "Common/FrameBuffer.hlsli" +#include "Common/VR.hlsli" + +cbuffer VolumetricFogCB : register(b0) +{ + uint4 VolumetricFogGridSizeAndFlags; + float4 VolumetricFogInvGridSizeAndNearFade; + float4 VolumetricFogGridZParams; + row_major float4x4 VolumetricFogClipToWorld[2]; + float4 VolumetricFogFrameJitterOffsets[16]; + float4 VolumetricFogHistoryParameters; + float4 VolumetricFogJitterParameters; +}; + +#define VolumetricFogGridSize VolumetricFogGridSizeAndFlags.xyz +#define VolumetricFogHasDirectionalShadowMap ((VolumetricFogGridSizeAndFlags.w & 1u) != 0u) +#define VolumetricFogHasConservativeDepth ((VolumetricFogGridSizeAndFlags.w & 2u) != 0u) +#define VolumetricFogHasIBL ((VolumetricFogGridSizeAndFlags.w & 4u) != 0u) +#define VolumetricFogHasSkylighting ((VolumetricFogGridSizeAndFlags.w & 8u) != 0u) +#define VolumetricFogHasPrevConservativeDepth ((VolumetricFogGridSizeAndFlags.w & 16u) != 0u) +#define VolumetricFogHasLocalLights ((VolumetricFogGridSizeAndFlags.w & 32u) != 0u) +#define VolumetricFogInvGridSize VolumetricFogInvGridSizeAndNearFade.xyz +#define VolumetricFogNearFadeInDistanceInv VolumetricFogInvGridSizeAndNearFade.w +#define VolumetricFogHistoryWeight VolumetricFogHistoryParameters.x +#define VolumetricFogHistoryMissSampleCount max(1u, min(16u, (uint)(VolumetricFogHistoryParameters.y + 0.5f))) +#define VolumetricFogSampleJitterMultiplier VolumetricFogJitterParameters.x +#define VolumetricFogStateFrameIndexMod8 ((uint)(VolumetricFogJitterParameters.y + 0.5f)) + +#define EXP_HEIGHT_FOG_GRID_SIZE_Z VolumetricFogGridSizeAndFlags.z +#define EXP_HEIGHT_FOG_GRID_Z_PARAMS VolumetricFogGridZParams.xyz +#include "ExponentialHeightFog/VolumetricFogCommon.hlsli" + +namespace ExponentialHeightFog +{ + bool IsInsideVolumetricGrid(uint3 coord) + { + return all(coord < VolumetricFogGridSize); + } + + float3 ComputeCellWorldPosition(uint3 coord, float3 cellOffset, out uint eyeIndex, out float viewDepth) + { + float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; + eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUV); + float2 eyeUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); + + viewDepth = ComputeVolumetricSliceDepth(max(float(coord.z) + cellOffset.z, 0.0f)); + + float2 ndc = eyeUV * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); + float deviceZ = (SharedData::CameraData.x - SharedData::CameraData.w / viewDepth) / SharedData::CameraData.z; + float4 worldPosition = mul(VolumetricFogClipToWorld[eyeIndex], float4(ndc, deviceZ, 1.0f)); + return worldPosition.xyz / worldPosition.w; + } +} + +#endif diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCommon.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCommon.hlsli new file mode 100644 index 0000000000..c963a9a891 --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCommon.hlsli @@ -0,0 +1,101 @@ +#ifndef __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_COMMON_HLSLI__ +#define __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_COMMON_HLSLI__ + +#include "Common/Math.hlsli" +#include "Common/SharedData.hlsli" + +namespace ExponentialHeightFog +{ + float HenyeyGreenstein(float cosTheta, float g) + { + float g2 = g * g; + float denom = 1.0f + g2 - 2.0f * g * cosTheta; + return (1.0f - g2) / (4.0f * Math::PI * pow(max(denom, 1e-5f), 1.5f)); + } + + float GetHeightFogFalloff() + { + return SharedData::exponentialHeightFogSettings.fogHeightFalloff * 0.001f; + } + + float GetHeightFogDensity() + { + return SharedData::exponentialHeightFogSettings.fogDensity * 0.001f; + } + + float GetVolumetricStartDistance() + { + return max(0.0f, SharedData::exponentialHeightFogSettings.volumetricFogStartDistance); + } + + float GetVolumetricEndDistance() + { + return max(GetVolumetricStartDistance() + 1.0f, SharedData::exponentialHeightFogSettings.volumetricFogDistance); + } + + float GetVolumetricGridSizeZ() + { +#if defined(EXP_HEIGHT_FOG_GRID_SIZE_Z) + return clamp(float(EXP_HEIGHT_FOG_GRID_SIZE_Z), 16.0f, 160.0f); +#else + return clamp(float(SharedData::exponentialHeightFogSettings.volumetricGridSizeZ), 16.0f, 160.0f); +#endif + } + + float GetVolumetricDepthDistributionScale() + { + return max(SharedData::exponentialHeightFogSettings.volumetricDepthDistributionScale, GetVolumetricGridSizeZ() / 120.0f); + } + + float3 GetVolumetricGridZParams(float gridSizeZ) + { +#if defined(EXP_HEIGHT_FOG_GRID_Z_PARAMS) + return EXP_HEIGHT_FOG_GRID_Z_PARAMS; +#else + gridSizeZ = clamp(gridSizeZ, 16.0f, 160.0f); + float nearPlane = max(SharedData::CameraData.y, GetVolumetricStartDistance()); + float farPlane = max(nearPlane + 1.0f, GetVolumetricEndDistance()); + float nearWithOffset = nearPlane + 0.095f * 100.0f; + float farExp = exp2(min(gridSizeZ / GetVolumetricDepthDistributionScale(), 120.0f)); + float gridZOffset = (farPlane - nearWithOffset * farExp) / (farPlane - nearWithOffset); + float gridZScale = (1.0f - gridZOffset) / nearWithOffset; + return float3(gridZScale, gridZOffset, GetVolumetricDepthDistributionScale()); +#endif + } + + float3 GetVolumetricGridZParams() + { + return GetVolumetricGridZParams(GetVolumetricGridSizeZ()); + } + + float ComputeVolumetricSliceDepth(float slice) + { + float3 gridZParams = GetVolumetricGridZParams(); + float sliceExp = exp2(min(slice / max(gridZParams.z, 1e-4f), 120.0f)); + return (sliceExp - gridZParams.y) / max(gridZParams.x, 1e-20f); + } + + float ComputeVolumetricNormalizedSlice(float viewDepth, float gridSizeZ) + { + gridSizeZ = clamp(gridSizeZ, 16.0f, 160.0f); + float3 gridZParams = GetVolumetricGridZParams(gridSizeZ); + return log2(max(viewDepth * gridZParams.x + gridZParams.y, 1e-6f)) * gridZParams.z / gridSizeZ; + } + + float ComputeVolumetricNormalizedSlice(float viewDepth) + { + return ComputeVolumetricNormalizedSlice(viewDepth, GetVolumetricGridSizeZ()); + } + + float EvaluateHeightFogExtinction(float3 positionWS, float3 cameraWS) + { + float fogDensity = GetHeightFogDensity(); + float fogHeightFalloff = GetHeightFogFalloff(); + float worldHeight = positionWS.z + cameraWS.z; + float exponent = fogHeightFalloff * max(worldHeight - SharedData::exponentialHeightFogSettings.fogHeight, 0.0f); + float localDensity = fogDensity * exp2(-exponent); + return max(localDensity * SharedData::exponentialHeightFogSettings.volumetricFogExtinctionScale * 0.5f, 0.0f); + } +} + +#endif diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl new file mode 100644 index 0000000000..7a87efa39e --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl @@ -0,0 +1,34 @@ +#include "ExponentialHeightFog/VolumetricFogCSCommon.hlsli" + +RWTexture2D ConservativeDepthTexture : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { + if (any(dispatchID.xy >= VolumetricFogGridSize.xy)) + return; + + float2 volumeUVMin = (float2(dispatchID.xy) - 0.5f.xx) * VolumetricFogInvGridSize.xy; + float2 volumeUVMax = (float2(dispatchID.xy + 1u) + 0.5f.xx) * VolumetricFogInvGridSize.xy; + float2 volumeUVCenter = (float2(dispatchID.xy) + 0.5f.xx) * VolumetricFogInvGridSize.xy; + + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUVCenter); + float2 eyeUVMin = saturate(Stereo::ConvertFromStereoUV(volumeUVMin, eyeIndex)); + float2 eyeUVMax = saturate(Stereo::ConvertFromStereoUV(volumeUVMax, eyeIndex)); + + int2 minCoord = SharedData::ConvertUVToSampleCoord(min(eyeUVMin, eyeUVMax), eyeIndex).xy; + int2 maxCoord = SharedData::ConvertUVToSampleCoord(max(eyeUVMin, eyeUVMax), eyeIndex).xy - 1; + maxCoord = max(maxCoord, minCoord); + + int2 bufferMax = int2(SharedData::BufferDim.xy) - 1; + minCoord = clamp(minCoord, int2(0, 0), bufferMax); + maxCoord = clamp(maxCoord, int2(0, 0), bufferMax); + + float conservativeDepth = 0.0f; + for (int y = minCoord.y; y <= maxCoord.y; y++) { + for (int x = minCoord.x; x <= maxCoord.x; x++) { + float rawDepth = SharedData::DepthTexture.Load(int3(x, y, 0)).x; + conservativeDepth = max(conservativeDepth, SharedData::GetScreenDepth(rawDepth)); + } + } + + ConservativeDepthTexture[dispatchID.xy] = conservativeDepth; +} diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl new file mode 100644 index 0000000000..e009d7885b --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl @@ -0,0 +1,42 @@ +Texture3D LightScattering : register(t0); +RWTexture3D IntegratedLightScattering : register(u0); + +#include "ExponentialHeightFog/VolumetricFogCSCommon.hlsli" + +[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { + if (any(dispatchID.xy >= VolumetricFogGridSize.xy)) + return; + + float3 accumulatedLighting = 0.0f.xxx; + float accumulatedTransmittance = 1.0f; + float accumulatedDepth = 0.0f; + + uint eyeIndex; + float previousDepth; + float3 previousPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(uint3(dispatchID.xy, 0), float3(0.5f, 0.5f, 0.0f), eyeIndex, previousDepth); + + [loop] for (uint layerIndex = 0; layerIndex < VolumetricFogGridSize.z; layerIndex++) + { + uint3 layerCoordinate = uint3(dispatchID.xy, layerIndex); + float4 scatteringAndExtinction = LightScattering[layerCoordinate]; + + uint layerEyeIndex; + float layerDepth; + float3 layerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(layerCoordinate, 0.5f.xxx, layerEyeIndex, layerDepth); + float stepLength = length(layerPositionWS - previousPositionWS); + previousPositionWS = layerPositionWS; + + float extinction = max(scatteringAndExtinction.w, 0.0f); + float transmittance = exp(-extinction * stepLength); + + accumulatedDepth += stepLength; + float fadeIn = saturate(accumulatedDepth * VolumetricFogNearFadeInDistanceInv); + + float3 scatteringIntegratedOverSlice = + fadeIn * (scatteringAndExtinction.rgb - scatteringAndExtinction.rgb * transmittance) / max(extinction, 1e-5f); + accumulatedLighting += scatteringIntegratedOverSlice * accumulatedTransmittance; + accumulatedTransmittance *= lerp(1.0f, transmittance, fadeIn); + + IntegratedLightScattering[layerCoordinate] = float4(max(accumulatedLighting, 0.0f.xxx), saturate(accumulatedTransmittance)); + } +} diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl new file mode 100644 index 0000000000..9263c88ba8 --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl @@ -0,0 +1,429 @@ +SamplerState LinearSampler : register(s0); +SamplerComparisonState ShadowSampler : register(s1); +Texture3D VBufferA : register(t0); +Texture2DArray DirectionalShadowMap : register(t1); +Texture3D LightScatteringHistory : register(t2); +Texture2D ConservativeDepthTexture : register(t3); +Texture2D PrevConservativeDepthTexture : register(t4); +RWTexture3D LightScattering : register(u0); + +#include "Common/Random.hlsli" +#include "ExponentialHeightFog/VolumetricFogCSCommon.hlsli" +#include "IBL/IBL.hlsli" +#if defined(TERRAIN_SHADOWS) +# include "TerrainShadows/TerrainShadows.hlsli" +#endif +#if defined(CLOUD_SHADOWS) +# include "CloudShadows/CloudShadows.hlsli" +#endif +#if defined(LIGHT_LIMIT_FIX) +# include "LightLimitFix/LightLimitFix.hlsli" +# include "InverseSquareLighting/InverseSquareLighting.hlsli" +#endif +#define SKYLIGHTING_PROBE_REGISTER t50 +#include "Skylighting/Skylighting.hlsli" + +struct DirectionalShadowLightData +{ + column_major float4x4 ShadowProj[2]; + column_major float4x4 InvShadowProj[2]; + float2 EndSplitDistances; + float2 StartSplitDistances; +}; + +StructuredBuffer DirectionalShadowLights : register(t98); + +// 4D PCG hash matching UE's Rand4DPCG32 (jcgt.org/published/0009/03/02/) +uint4 Rand4DPCG32(int4 p) +{ + uint4 v = uint4(p); + v = v * 1664525u + 1013904223u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + v ^= (v >> 16u); + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + return v; +} + +// Matches UE's MakePositiveFinite - ensures no NaN/Inf propagates into history chain +float4 MakePositiveFinite(float4 v) +{ + v = max(v, 0.0f.xxxx); + v.x = isfinite(v.x) ? v.x : 0.0f; + v.y = isfinite(v.y) ? v.y : 0.0f; + v.z = isfinite(v.z) ? v.z : 0.0f; + v.w = isfinite(v.w) ? v.w : 0.0f; + return v; +} + +bool IsFroxelBehindSceneDepth(uint3 coord) +{ + float frontDepth = ExponentialHeightFog::ComputeVolumetricSliceDepth(max(float(coord.z) - 0.5f, 0.0f)); + float sceneDepth = ConservativeDepthTexture[coord.xy]; + return sceneDepth < frontDepth; +} + +float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, uint eyeIndex, out bool validHistory, out float previousViewDepth) +{ + float3 previousPositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPreviousPosAdjust[eyeIndex].xyz; + float4 previousClip = mul(FrameBuffer::CameraPreviousViewProjUnjittered[eyeIndex], float4(previousPositionWS, 1.0f)); + + previousViewDepth = abs(previousClip.w); + validHistory = previousClip.w > 0.0f; + if (!validHistory) + return 0.0f.xxx; + + float2 historyUV = previousClip.xy / previousClip.w * float2(0.5f, -0.5f) + 0.5f; +#if defined(VR) + historyUV = Stereo::ConvertToStereoUV(historyUV, eyeIndex); +#endif + + float historyZ = ExponentialHeightFog::ComputeVolumetricNormalizedSlice(previousViewDepth); + float3 volumeUV = float3(historyUV, historyZ); + validHistory = !any(volumeUV < 0.0f) && !any(volumeUV >= 1.0f); + return saturate(volumeUV); +} + +float3 ComputeHistoryVolumeUV(float3 positionWS, uint eyeIndex, out bool validHistory) +{ + float previousViewDepth; + return ComputeHistoryVolumeUVAndDepth(positionWS, eyeIndex, validHistory, previousViewDepth); +} + +float2 FixupHistoryUV(float2 uv, float previousCellDepth, out bool validHistory) +{ + float2 size = float2(VolumetricFogGridSize.xy); + float2 fullResUV = uv * size; + float2 screenCoord = floor(fullResUV - 0.5f); + float2 fullResOffset = fullResUV - screenCoord; + float2 gatherUV = (screenCoord + 1.0f) / size; + + float4 previousSceneDepths = PrevConservativeDepthTexture.Gather(LinearSampler, gatherUV); + bool4 validSamples = previousSceneDepths >= previousCellDepth; + + validHistory = true; + if (all(validSamples)) + return uv; + + if (all(validSamples.wz)) + return (screenCoord + float2(fullResOffset.x, 0.5f)) / size; + if (all(validSamples.xy)) + return (screenCoord + float2(fullResOffset.x, 1.5f)) / size; + if (all(validSamples.wx)) + return (screenCoord + float2(0.5f, fullResOffset.y)) / size; + if (all(validSamples.zy)) + return (screenCoord + float2(1.5f, fullResOffset.y)) / size; + + if (validSamples.x) + return (screenCoord + float2(0.5f, 1.5f)) / size; + if (validSamples.y) + return (screenCoord + float2(1.5f, 1.5f)) / size; + if (validSamples.w) + return (screenCoord + float2(0.5f, 0.5f)) / size; + if (validSamples.z) + return (screenCoord + float2(1.5f, 0.5f)) / size; + + validHistory = false; + return uv; +} + +float SampleDirectionalShadowPCF(float3 positionLS, uint cascadeIndex) +{ + uint shadowWidth; + uint shadowHeight; + uint shadowSlices; + DirectionalShadowMap.GetDimensions(shadowWidth, shadowHeight, shadowSlices); + if (cascadeIndex >= shadowSlices) + return 1.0f; + + float2 texelSize = rcp(float2(max(shadowWidth, 1), max(shadowHeight, 1))); + float compareDepth = positionLS.z - SharedData::exponentialHeightFogSettings.volumetricShadowBias; + + float2 uvMin = texelSize * 1.5f; + float2 uvMax = 1.0f.xx - uvMin; + if (any(positionLS.xy < uvMin) || any(positionLS.xy > uvMax)) + return DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(saturate(positionLS.xy), cascadeIndex), compareDepth).x; + + float center = DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(positionLS.xy, cascadeIndex), compareDepth).x; + float cross = + DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(positionLS.xy + float2(texelSize.x, 0.0f), cascadeIndex), compareDepth).x + + DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(positionLS.xy - float2(texelSize.x, 0.0f), cascadeIndex), compareDepth).x + + DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(positionLS.xy + float2(0.0f, texelSize.y), cascadeIndex), compareDepth).x + + DirectionalShadowMap.SampleCmpLevelZero(ShadowSampler, float3(positionLS.xy - float2(0.0f, texelSize.y), cascadeIndex), compareDepth).x; + + return (center * 4.0f + cross) * rcp(8.0f); +} + +float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) +{ + if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) + return 1.0f; + if (!VolumetricFogHasDirectionalShadowMap) + return 1.0f; + + DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(positionWS, eyeIndex)); + if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) + return 1.0f; + + float splitDenom = max(directionalShadowLightData.EndSplitDistances.x - directionalShadowLightData.StartSplitDistances.y, 1e-4f); + float cascadeSelect = smoothstep(0.0f, 1.0f, saturate((shadowMapDepth - directionalShadowLightData.StartSplitDistances.y) / splitDenom)); + uint primaryCascade = (uint)cascadeSelect; + + float3 absolutePositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 positionLS = mul(directionalShadowLightData.ShadowProj[primaryCascade], float4(absolutePositionWS, 1.0f)).xyz; + if (any(positionLS.xy < 0.0f) || any(positionLS.xy > 1.0f)) + return 1.0f; + + float shadow = SampleDirectionalShadowPCF(positionLS, primaryCascade); + + [branch] if (cascadeSelect > 0.0f && cascadeSelect < 1.0f) + { + uint secondaryCascade = 1u - primaryCascade; + float3 secondaryLS = mul(directionalShadowLightData.ShadowProj[secondaryCascade], float4(absolutePositionWS, 1.0f)).xyz; + if (!any(secondaryLS.xy < 0.0f) && !any(secondaryLS.xy > 1.0f)) { + float secondaryShadow = SampleDirectionalShadowPCF(secondaryLS, secondaryCascade); + shadow = lerp(shadow, secondaryShadow, cascadeSelect); + } + } + + float fade = saturate(shadowMapDepth / max(directionalShadowLightData.EndSplitDistances.y, 1.0f)); + float fadeFactor = 1.0f - pow(fade * fade, 8.0f); + return lerp(1.0f, shadow, fadeFactor); +} + +float SampleDirectionalWorldShadow(float3 positionWS, uint eyeIndex) +{ + if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) + return 1.0f; + + float worldShadow = 1.0f; +#if defined(TERRAIN_SHADOWS) + worldShadow *= TerrainShadows::GetTerrainShadow(positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, LinearSampler); +#endif +#if defined(CLOUD_SHADOWS) + worldShadow *= CloudShadows::GetCloudShadowMult(positionWS, LinearSampler); +#endif + return worldShadow; +} + +float3 ComputeSkyLightScattering(float3 positionWS, float3 viewDirection, uint eyeIndex) +{ + float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; + float3 skyDirection = abs(phaseG) > 0.001f ? normalize(-viewDirection * phaseG) : 0.0f.xxx; + float3 skyVisibilityDirection = abs(phaseG) > 0.001f ? skyDirection : float3(0.0f, 0.0f, 1.0f); + float skyVisibility = 1.0f; + if (VolumetricFogHasSkylighting && !SharedData::InInterior) { +#if defined(VR) + float3 skylightingPosition = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +#else + float3 skylightingPosition = positionWS; +#endif + sh2 skylightingSH = Skylighting::SampleNoBias(skylightingPosition); + skyVisibility = Skylighting::EvaluateDiffuse(skylightingSH, skyVisibilityDirection, Skylighting::GetFadeOutFactor(skylightingPosition)); + } + + float3 skyLighting = + SharedData::exponentialHeightFogSettings.fogInscatteringColor.rgb * + SharedData::exponentialHeightFogSettings.fogInscatteringColor.a * + skyVisibility; + [branch] if (VolumetricFogHasIBL) + skyLighting = ImageBasedLighting::GetIBLColorOccluded(skyDirection, skyVisibility); + + return skyLighting * + SharedData::exponentialHeightFogSettings.volumetricSkyLightingIntensity; +} + +#if defined(LIGHT_LIMIT_FIX) +float ComputeLocalLightAttenuation(float distanceSqr, float cellRadius, LightLimitFix::Light light) +{ + float distance = sqrt(max(distanceSqr, 1e-6f)); + + // UE biases local light integration by froxel size to avoid singular bright voxels close to the light. + if (light.lightFlags & LightLimitFix::LightFlags::InverseSquare) { + distance = sqrt(max(distanceSqr, cellRadius * cellRadius)); + } + + return InverseSquareLighting::GetAttenuation(distance, light); +} + +float3 AccumulateLocalLightScattering( + uint3 coord, + float3 cellOffset, + float3 positionWS, + float viewDepth, + float3 viewDirection, + uint eyeIndex, + float3 materialScattering) +{ + if (!VolumetricFogHasLocalLights) + return 0.0f.xxx; + + float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; + float2 screenUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); + + uint clusterIndex = 0; + if (!LightLimitFix::GetClusterIndex(screenUV, viewDepth, clusterIndex)) + return 0.0f.xxx; + + LightLimitFix::LightGrid grid = LightLimitFix::lightGrid[clusterIndex]; + uint lightCount = min(grid.lightCount, (uint)MAX_CLUSTER_LIGHTS); + + uint cornerEyeIndex; + float cornerViewDepth; + float3 cellCornerWS = ExponentialHeightFog::ComputeCellWorldPosition(coord + uint3(1, 1, 1), cellOffset, cornerEyeIndex, cornerViewDepth); + float cellRadius = max(length(cellCornerWS - positionWS), 1.0f); + + float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; + float3 localScattering = 0.0f.xxx; + [loop] for (uint lightIndex = 0; lightIndex < lightCount; lightIndex++) + { + uint clusteredLightIndex = LightLimitFix::lightList[grid.offset + lightIndex]; + LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; + + if (light.lightFlags & LightLimitFix::LightFlags::Disabled) + continue; + + float3 toLight = light.positionWS[eyeIndex].xyz - positionWS; + float distanceSqr = dot(toLight, toLight); + if (distanceSqr < 1e-6f) + continue; + + float attenuation = ComputeLocalLightAttenuation(distanceSqr, cellRadius, light); + if (attenuation < 1e-5f) + continue; + + float3 L = toLight * rsqrt(distanceSqr); + float phase = ExponentialHeightFog::HenyeyGreenstein(dot(L, -viewDirection), phaseG); + + const bool isPointLightLinear = light.lightFlags & LightLimitFix::LightFlags::Linear; + float3 lightColor = Color::PointLight(light.color.xyz, isPointLightLinear) * attenuation * light.fade; + localScattering += lightColor * phase; + } + + return localScattering * + SharedData::exponentialHeightFogSettings.volumetricLocalLightScatteringIntensity * + materialScattering; +} +#else +float3 AccumulateLocalLightScattering( + uint3 coord, + float3 cellOffset, + float3 positionWS, + float viewDepth, + float3 viewDirection, + uint eyeIndex, + float3 materialScattering) +{ + return 0.0f.xxx; +} +#endif + +float4 ComputeLightScattering(uint3 coord, float3 cellOffset) +{ + uint eyeIndex; + float viewDepth; + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(coord, cellOffset, eyeIndex, viewDepth); + + float4 materialScatteringAndExtinction = VBufferA[coord]; + float extinction = materialScatteringAndExtinction.w; + + float3 viewDirection = normalize(positionWS); + + // Directional light uses isotropic phase (1/4PI) in the volume to avoid angular aliasing + // at the coarse froxel XY resolution. The actual per-pixel HG phase is applied at full + // resolution during compositing in SampleVolumetricFog(). + float directionalPhase = 1.0f / (4.0f * Math::PI); + + float directionalShadow = SampleDirectionalShadow(positionWS, eyeIndex) * + SampleDirectionalWorldShadow(positionWS, eyeIndex); + float3 directionalScattering = + SharedData::DirLightColor.xyz * + SharedData::exponentialHeightFogSettings.volumetricDirectionalScatteringIntensity * + directionalShadow * + directionalPhase * + materialScatteringAndExtinction.rgb; + + float3 skyScattering = ComputeSkyLightScattering(positionWS, viewDirection, eyeIndex) * + materialScatteringAndExtinction.rgb; + + float3 localScattering = AccumulateLocalLightScattering( + coord, + cellOffset, + positionWS, + viewDepth, + viewDirection, + eyeIndex, + materialScatteringAndExtinction.rgb); + + float3 emissive = SharedData::exponentialHeightFogSettings.volumetricFogEmissive.rgb * + SharedData::exponentialHeightFogSettings.volumetricFogEmissive.a * + extinction; + + return float4(max(directionalScattering + skyScattering + localScattering + emissive, 0.0f.xxx), extinction); +} + +[numthreads(8, 8, 4)] void main(uint3 dispatchID : SV_DispatchThreadID) { + if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) + return; + + uint eyeIndex; + float viewDepth; + float3 centerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); + if (VolumetricFogHasConservativeDepth && IsFroxelBehindSceneDepth(dispatchID)) { + LightScattering[dispatchID] = 0.0f.xxxx; + return; + } + + bool validHistory; + float3 historyUV = ComputeHistoryVolumeUV(centerPositionWS, eyeIndex, validHistory); + if (VolumetricFogHasPrevConservativeDepth && validHistory) { + uint frontEyeIndex; + float frontDepth; + float3 frontPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, float3(0.5f, 0.5f, -0.5f), frontEyeIndex, frontDepth); + bool validFrontHistory; + float previousFrontDepth; + ComputeHistoryVolumeUVAndDepth(frontPositionWS, frontEyeIndex, validFrontHistory, previousFrontDepth); + if (validFrontHistory) { + historyUV.xy = saturate(FixupHistoryUV(historyUV.xy, previousFrontDepth, validHistory)); + } else { + validHistory = false; + } + } + + float historyAlpha = VolumetricFogHistoryWeight; + [flatten] if (!validHistory || any(historyUV < 0.0f) || any(historyUV >= 1.0f)) + { + historyAlpha = 0.0f; + } + + uint sampleCount = historyAlpha < 0.001f ? VolumetricFogHistoryMissSampleCount : 1u; + float4 scatteringAndExtinction = 0.0f.xxxx; + [loop] for (uint sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) + { + // Per-voxel random noise matching UE's LightScatteringCS: + // Rand4DPCG32(int4(GridCoordinate.xyz, StateFrameIndexMod8 + 8 * SampleIndex)) + // This decorrelates the jitter pattern across voxels, preventing coherent temporal artifacts + uint3 Rand32Bits = Rand4DPCG32(int4(dispatchID.xyz, VolumetricFogStateFrameIndexMod8 + 8 * sampleIndex)).xyz; + float3 Rand3D = (float3(Rand32Bits) / float(uint(0xffffffff))) * 2.0f - 1.0f; + float3 cellOffset = VolumetricFogFrameJitterOffsets[sampleIndex].xyz + VolumetricFogSampleJitterMultiplier * Rand3D; + + scatteringAndExtinction += ComputeLightScattering(dispatchID, cellOffset); + } + scatteringAndExtinction *= rcp(float(sampleCount)); + + [branch] if (historyAlpha > 0.0f) + { + float4 history = LightScatteringHistory.SampleLevel(LinearSampler, historyUV, 0); + // Sanitize history to prevent NaN/Inf propagation in the temporal chain + history = MakePositiveFinite(history); + scatteringAndExtinction = lerp(scatteringAndExtinction, history, historyAlpha); + } + + LightScattering[dispatchID] = MakePositiveFinite(scatteringAndExtinction); +} diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl new file mode 100644 index 0000000000..07687f7015 --- /dev/null +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl @@ -0,0 +1,18 @@ +#include "ExponentialHeightFog/VolumetricFogCSCommon.hlsli" + +RWTexture3D VBufferA : register(u0); + +[numthreads(8, 8, 4)] void main(uint3 dispatchID : SV_DispatchThreadID) { + if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) + return; + + uint eyeIndex; + float viewDepth; + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); + + float extinction = ExponentialHeightFog::EvaluateHeightFogExtinction(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + float3 albedo = saturate(SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.rgb); + float3 scattering = extinction * albedo * SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.a; + + VBufferA[dispatchID] = float4(scattering, extinction); +} diff --git a/features/Exponential Height Fog/Shaders/Features/ExponentialHeightFog.ini b/features/Exponential Height Fog/Shaders/Features/ExponentialHeightFog.ini index 9e325f8475..0a7412150c 100644 --- a/features/Exponential Height Fog/Shaders/Features/ExponentialHeightFog.ini +++ b/features/Exponential Height Fog/Shaders/Features/ExponentialHeightFog.ini @@ -1,5 +1,5 @@ [Info] -Version = 1-2-0 +Version = 1-3-0 [Nexus] autoupload = false diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 7ae1794d11..36de2bcfe3 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -567,21 +567,44 @@ "feature.exp_height_fog.apply_vanilla_fade": "Apply Vanilla Fade", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "Applies vanilla fade brightness to exponential height fog.", "feature.exp_height_fog.cubemap_mip_level": "Cubemap Mip Level", + "feature.exp_height_fog.debug": "Debug", + "feature.exp_height_fog.depth_distribution_scale": "Depth Distribution Scale", "feature.exp_height_fog.dir_inscattering_anisotropy": "Directional Light Inscattering Anisotropy", "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "Controls the asymmetry of inscattering via the Henyey-Greenstein phase function.\nPositive values produce forward scattering (glow around sun).\nZero is isotropic. Negative values produce back scattering.", "feature.exp_height_fog.dir_inscattering_mul": "Directional Light Inscattering Multiplier", + "feature.exp_height_fog.directional_scattering_intensity": "Directional Scattering Intensity", + "feature.exp_height_fog.directional_shadow_bias": "Directional Shadow Bias", "feature.exp_height_fog.disable_vanilla_fog": "Disable Vanilla Fog", "feature.exp_height_fog.disable_vanilla_fog_tooltip": "Disables the vanilla fog entirely. Only exponential height fog will be applied.", "feature.exp_height_fog.enable_exp_height_fog": "Enable Exponential Height Fog", + "feature.exp_height_fog.enable_volumetric_fog": "Enable Volumetric Fog", "feature.exp_height_fog.fog_density": "Fog Density", "feature.exp_height_fog.fog_height": "Fog Height", "feature.exp_height_fog.fog_height_falloff": "Fog Height Falloff", "feature.exp_height_fog.fog_inscattering_color": "Fog Inscattering Color", + "feature.exp_height_fog.grid_depth_slices": "Grid Depth Slices", + "feature.exp_height_fog.grid_pixel_size": "Grid Pixel Size", + "feature.exp_height_fog.history_miss_samples": "History Miss Samples", "feature.exp_height_fog.inscattering_cubemap_tint": "Inscattering Cubemap Tint", + "feature.exp_height_fog.local_light_scattering_intensity": "Local Light Scattering Intensity", + "feature.exp_height_fog.near_fade_in_distance": "Near Fade In Distance", "feature.exp_height_fog.original_fog_color_amount": "Original Fog Color Amount", + "feature.exp_height_fog.sample_jitter_multiplier": "Sample Jitter Multiplier", + "feature.exp_height_fog.sample_jitter_multiplier_tooltip": "Matches UE's r.VolumetricFog.LightScatteringSampleJitterMultiplier.\nAdds per-voxel random offset on top of the Halton sequence.\n0 = UE default; nonzero values need stronger temporal filtering.", + "feature.exp_height_fog.sky_lighting_scattering_intensity": "Sky Lighting Scattering Intensity", "feature.exp_height_fog.start_distance": "Start Distance", "feature.exp_height_fog.sunlight_attenuation": "Sunlight Attenuation Amount", + "feature.exp_height_fog.temporal_history_weight": "Temporal History Weight", + "feature.exp_height_fog.upsample_jitter_multiplier": "Upsample Jitter Multiplier", + "feature.exp_height_fog.upsample_jitter_multiplier_tooltip": "Matches UE's r.VolumetricFog.UpsampleJitterMultiplier.\nJitters the final 3D fog lookup in screen space to hide\nlow-resolution froxel pixelization. 0 = UE default.", "feature.exp_height_fog.use_dynamic_cubemaps": "Use Dynamic Cubemaps for Inscattering", + "feature.exp_height_fog.volumetric_albedo": "Volumetric Albedo", + "feature.exp_height_fog.volumetric_emissive": "Volumetric Emissive", + "feature.exp_height_fog.volumetric_extinction_scale": "Volumetric Extinction Scale", + "feature.exp_height_fog.volumetric_fog": "Volumetric Fog", + "feature.exp_height_fog.volumetric_scattering_distribution": "Volumetric Scattering Distribution", + "feature.exp_height_fog.volumetric_start_distance": "Volumetric Start Distance", + "feature.exp_height_fog.volumetric_view_distance": "Volumetric View Distance", "feature.exponential_height_fog.description": "Exponential Height Fog adds a realistic fog effect that increases in density with height, enhancing atmospheric depth and immersion in the game environment.", "feature.exponential_height_fog.key_feature_1": "Added exponential height fog effect", "feature.exponential_height_fog.key_feature_2": "Adapted to vanilla fog settings", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 0f38a04c23..f5c36c0f7f 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -48,21 +48,44 @@ "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", + "feature.exp_height_fog.debug": "调试", + "feature.exp_height_fog.depth_distribution_scale": "深度分布比例", "feature.exp_height_fog.dir_inscattering_anisotropy": "方向光内散射各向异性", "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "通过Henyey-Greenstein相位函数控制内散射的不对称性。\n正值产生前向散射(太阳周围发光)。\n零为各向同性。负值产生后向散射。", "feature.exp_height_fog.dir_inscattering_mul": "方向光内散射倍率", + "feature.exp_height_fog.directional_scattering_intensity": "方向光散射强度", + "feature.exp_height_fog.directional_shadow_bias": "方向阴影偏移", "feature.exp_height_fog.disable_vanilla_fog": "禁用原版雾", "feature.exp_height_fog.disable_vanilla_fog_tooltip": "完全禁用原版雾。仅应用指数高度雾。", "feature.exp_height_fog.enable_exp_height_fog": "启用指数高度雾", + "feature.exp_height_fog.enable_volumetric_fog": "启用体积雾", "feature.exp_height_fog.fog_density": "雾密度", "feature.exp_height_fog.fog_height": "雾高度", "feature.exp_height_fog.fog_height_falloff": "雾高度衰减", "feature.exp_height_fog.fog_inscattering_color": "雾内散射颜色", + "feature.exp_height_fog.grid_depth_slices": "网格深度切片", + "feature.exp_height_fog.grid_pixel_size": "网格像素大小", + "feature.exp_height_fog.history_miss_samples": "历史缺失采样数", "feature.exp_height_fog.inscattering_cubemap_tint": "内散射立方体贴图色调", + "feature.exp_height_fog.local_light_scattering_intensity": "局部光散射强度", + "feature.exp_height_fog.near_fade_in_distance": "近处淡入距离", "feature.exp_height_fog.original_fog_color_amount": "原始雾颜色量", + "feature.exp_height_fog.sample_jitter_multiplier": "采样抖动倍率", + "feature.exp_height_fog.sample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.LightScatteringSampleJitterMultiplier。\n在 Halton 序列基础上为每个体素添加随机偏移。\n0 = UE 默认值;非零值需要更强的时域滤波。", + "feature.exp_height_fog.sky_lighting_scattering_intensity": "天空光照散射强度", "feature.exp_height_fog.start_distance": "起始距离", "feature.exp_height_fog.sunlight_attenuation": "阳光衰减量", + "feature.exp_height_fog.temporal_history_weight": "时域历史权重", + "feature.exp_height_fog.upsample_jitter_multiplier": "上采样抖动倍率", + "feature.exp_height_fog.upsample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.UpsampleJitterMultiplier。\n在屏幕空间抖动最终 3D 雾查找,以隐藏\n低分辨率 froxel 像素化。0 = UE 默认值。", "feature.exp_height_fog.use_dynamic_cubemaps": "使用动态立方体贴图进行内散射", + "feature.exp_height_fog.volumetric_albedo": "体积雾反照率", + "feature.exp_height_fog.volumetric_emissive": "体积雾自发光", + "feature.exp_height_fog.volumetric_extinction_scale": "体积雾消光比例", + "feature.exp_height_fog.volumetric_fog": "体积雾", + "feature.exp_height_fog.volumetric_scattering_distribution": "体积雾散射分布", + "feature.exp_height_fog.volumetric_start_distance": "体积雾起始距离", + "feature.exp_height_fog.volumetric_view_distance": "体积雾视距", "feature.exponential_height_fog.description": "添加逼真的高度雾效果,雾密度随高度变化,增强场景的大气深度和沉浸感。", "feature.exponential_height_fog.key_feature_1": "新增指数高度雾效果", "feature.exponential_height_fog.key_feature_2": "适配原版雾效设置", @@ -492,7 +515,7 @@ "feature.screenshot.hdr_bit_depth_tooltip": "48 bpp RGB PNG 负载的量化位深。11位是较好的默认值;更高的值会增加文件大小,但收益递减。", "feature.screenshot.hdr_note": "HDR 已启用:将显示帧保存为带有 HDR10 元数据的 PNG(48 bpp RGB,cICP/cLLi)。请使用支持 HDR 的查看器,如 Windows 照片(HDR 开启)或 Special K SKIF。", "feature.screenshot.hotkey": "热键", - "feature.screenshot.hotkey_collision": "此热键与原版PrintScreen冲突;两者都会触发保存。\n在Skyrim.ini中设置bAllowScreenShot=0以抑制原版,或在上方选择不同的热键。", + "feature.screenshot.hotkey_collision": "此热键与原版PrintScreen冲突;两者都会触发保存。在Skyrim.ini中设置bAllowScreenShot=0以抑制原版,或在上方选择不同的热键。", "feature.screenshot.name": "截图", "feature.screenshot.open": "打开", "feature.screenshot.output": "输出", diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 05c257b034..e6ec03706b 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -266,7 +266,26 @@ namespace SharedData uint disableVanillaFog; float4 fogInscatteringColor; float originalFogColorAmount; - float3 pad; + uint volumetricFogEnabled; + uint volumetricGridPixelSize; + uint volumetricGridSizeZ; + float volumetricFogDistance; + float volumetricFogStartDistance; + float volumetricFogNearFadeInDistance; + float volumetricFogExtinctionScale; + float4 volumetricFogAlbedo; + float4 volumetricFogEmissive; + float volumetricDirectionalScatteringIntensity; + float volumetricShadowBias; + float volumetricDepthDistributionScale; + float volumetricSkyLightingIntensity; + float volumetricFogScatteringDistribution; + float volumetricHistoryWeight; + uint volumetricHistoryMissSampleCount; + float volumetricSampleJitterMultiplier; + float volumetricUpsampleJitterMultiplier; + float volumetricLocalLightScatteringIntensity; + float2 pad0; }; struct TruePBRSettings diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index 669d61342e..3e510142a2 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -2,6 +2,7 @@ #include "Common/FrameBuffer.hlsli" #include "Common/GBuffer.hlsli" #include "Common/MotionBlur.hlsli" +#include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "Common/VR.hlsli" @@ -180,6 +181,15 @@ const static float DepthOffsets[16] = { # include "Common/ShadowSampling.hlsli" +# if defined(EXP_HEIGHT_FOG) +void ApplyReflectionExponentialHeightFog(inout float3 color, float3 positionWS, float4 screenPosition, uint eyeIndex) +{ + float3 fogColor = Color::Fog(AmbientColor.xyz); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(screenPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, screenPosition.z, 1)); + color = lerp(color, exponentialHeightFog.xyz, exponentialHeightFog.w); +} +# endif + PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; @@ -189,6 +199,9 @@ PS_OUTPUT main(PS_INPUT input) # else uint eyeIndex = input.EyeIndex; # endif // !VR +# if defined(EXP_HEIGHT_FOG) + const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; +# endif # if defined(RENDER_DEPTH) uint2 temp = uint2(input.Position.xy); @@ -255,6 +268,12 @@ PS_OUTPUT main(PS_INPUT input) psout.Diffuse.xyz = diffuseColor * baseColor.xyz; psout.Diffuse.w = 1; +# if defined(EXP_HEIGHT_FOG) + if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { + ApplyReflectionExponentialHeightFog(psout.Diffuse.xyz, input.WorldPosition.xyz, input.Position, eyeIndex); + } +# endif + psout.MotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false, eyeIndex)); @@ -287,6 +306,11 @@ PS_OUTPUT main(PS_INPUT input) diffuseColor += directionalAmbientColor; float3 color = diffuseColor * baseColor.xyz; +# if defined(EXP_HEIGHT_FOG) + if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { + ApplyReflectionExponentialHeightFog(color, input.WorldPosition.xyz, input.Position, eyeIndex); + } +# endif psout.Diffuse = float4(color, 1.0); # endif // DEFERRED # endif // RENDER_DEPTH diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index a901e9bf2c..2094160a73 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -903,16 +903,18 @@ PS_OUTPUT main(PS_INPUT input) float3 vanillaFogColor = fogColor; float expFogFactor = 0; if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); expFogFactor = exponentialHeightFog.w; # if defined(ADDBLEND) || defined(MULTBLEND) || defined(MULTBLEND_DECAL) fogColor = exponentialHeightFog.xyz; fogFactor = exponentialHeightFog.w; # else - fogColor = lightColor; + fogColor = exponentialHeightFog.xyz; + fogFactor = exponentialHeightFog.w; alpha *= 1 - exponentialHeightFog.w; # endif if (ExponentialHeightFog::ShouldDisableVanillaFog()) { + vanillaFogColor = lightColor; vanillaFogFactor = 0; } } @@ -933,6 +935,7 @@ PS_OUTPUT main(PS_INPUT input) # else # if defined(EXP_HEIGHT_FOG) float3 blendedColor = lerp(lightColor, vanillaFogColor, vanillaFogFactor.xxx); + blendedColor = lerp(blendedColor, fogColor, fogFactor.xxx); # else float3 blendedColor = lerp(lightColor, fogColor, fogFactor.xxx); # endif diff --git a/package/Shaders/ISSAOComposite.hlsl b/package/Shaders/ISSAOComposite.hlsl index 55785c7604..ecaf54ce1e 100644 --- a/package/Shaders/ISSAOComposite.hlsl +++ b/package/Shaders/ISSAOComposite.hlsl @@ -196,7 +196,8 @@ PS_OUTPUT main(PS_INPUT input) positionWS.xyz = positionWS.xyz / positionWS.w; float4 exponentialHeightFog = (float4)0; if (exponentialHeightFogEnabled) { - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor); + float4 fogScreenPosition = float4(Stereo::ConvertToStereoUV(monoUV, eyeIndex) * SharedData::BufferDim.xy, depth, 1.0f); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, fogScreenPosition); } if (isGeometryDepth || exponentialHeightFogEnabled) { float fogFade = exponentialHeightFogEnabled ? ExponentialHeightFog::GetVanillaFogFade(FogNearColor.w) : FogNearColor.w; diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index d89703119f..fc3fbef07c 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -3283,12 +3283,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 vanillaFogColor = fogColor; float vanillaFogFactor = fogFactor; if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor); + float4 exponentialHeightFog; + if (inReflection) { + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + } else { + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + } fogColor = exponentialHeightFog.xyz; fogFactor = exponentialHeightFog.w; } # endif - if (FrameBuffer::FrameParams.y && FrameBuffer::FrameParams.z) { + if ((FrameBuffer::FrameParams.y && FrameBuffer::FrameParams.z) || inReflection) { # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { if (!ExponentialHeightFog::ShouldDisableVanillaFog()) { diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index a4c83b2f03..219962495b 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -45,6 +45,7 @@ struct VS_OUTPUT float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; + float3 FogPosition: TEXCOORD4; #if defined(VR) float ClipDistance: SV_ClipDistance0; // o11 float CullDistance: SV_CullDistance0; // p11 @@ -138,6 +139,7 @@ VS_OUTPUT main(VS_INPUT input) vsout.Position = mul(WorldViewProj[eyeIndex], inputPosition).xyww; vsout.WorldPosition = mul(World[eyeIndex], inputPosition); + vsout.FogPosition = vsout.WorldPosition.xyz - EyePosition[eyeIndex].xyz; vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], inputPosition); # ifdef VR @@ -282,6 +284,15 @@ PS_OUTPUT main(PS_INPUT input) psout.Color = float4(0, 0, 0, 1.0); # endif // OCCLUSION +# if defined(EXP_HEIGHT_FOG) + const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; + if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { + float3 skyFogPosition = normalize(input.FogPosition.xyz) * SharedData::CameraData.x; + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(skyFogPosition, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, psout.Color.xyz, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + psout.Color.xyz = lerp(psout.Color.xyz, exponentialHeightFog.xyz, exponentialHeightFog.w); + } +# endif + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); psout.MotionVectors = float4(screenMotionVector, 0, psout.Color.w); diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index 87927eaece..54998a24e3 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -1278,7 +1278,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { fogColor = exponentialHeightFog.xyz; fogColor *= GetWaterFogFade(eyeIndex); @@ -1329,7 +1329,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, preFogColor); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, preFogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { preFogColor = exponentialHeightFog.xyz; preFogColor *= GetWaterFogFade(eyeIndex); diff --git a/src/Features/ExponentialHeightFog.cpp b/src/Features/ExponentialHeightFog.cpp index cea43ff252..4eb4c1cb9d 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -1,6 +1,15 @@ #include "ExponentialHeightFog.h" +#include "Deferred.h" +#include "Features/CloudShadows.h" +#include "Features/IBL.h" +#include "Features/LightLimitFix.h" +#include "Features/Skylighting.h" +#include "Features/TerrainShadows.h" #include "I18n/I18n.h" +#include "State.h" +#include "Utils/D3D.h" +#include "Utils/Game.h" #include "WeatherVariableRegistry.h" #define I18N_KEY_PREFIX "feature.exp_height_fog." @@ -21,7 +30,42 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( respectVanillaFogFade, disableVanillaFog, fogInscatteringColor, - originalFogColorAmount) + originalFogColorAmount, + volumetricFogEnabled, + volumetricGridPixelSize, + volumetricGridSizeZ, + volumetricFogDistance, + volumetricFogStartDistance, + volumetricFogNearFadeInDistance, + volumetricFogExtinctionScale, + volumetricFogScatteringDistribution, + volumetricFogAlbedo, + volumetricFogEmissive, + volumetricDirectionalScatteringIntensity, + volumetricShadowBias, + volumetricDepthDistributionScale, + volumetricSkyLightingIntensity, + volumetricHistoryWeight, + volumetricHistoryMissSampleCount, + volumetricSampleJitterMultiplier, + volumetricUpsampleJitterMultiplier, + volumetricLocalLightScatteringIntensity) + +namespace +{ + float Halton(uint32_t a_index, uint32_t a_base) + { + float result = 0.0f; + float invBase = 1.0f / static_cast(a_base); + float fraction = invBase; + while (a_index > 0) { + result += static_cast(a_index % a_base) * fraction; + a_index /= a_base; + fraction *= invBase; + } + return result; + } +} void ExponentialHeightFog::RestoreDefaultSettings() { @@ -67,6 +111,475 @@ void ExponentialHeightFog::DrawSettings() ImGui::Checkbox(T(TKEY("use_dynamic_cubemaps"), "Use Dynamic Cubemaps for Inscattering"), (bool*)&settings.useDynamicCubemaps); Util::WeatherUI::ColorEdit4(T(TKEY("inscattering_cubemap_tint"), "Inscattering Cubemap Tint"), this, "inscatteringTint", (float*)&settings.inscatteringTint); ImGui::SliderFloat(T(TKEY("cubemap_mip_level"), "Cubemap Mip Level"), &settings.cubemapMipLevel, 1.0f, 7.0f, "%.1f"); + + ImGui::SeparatorText(T(TKEY("volumetric_fog"), "Volumetric Fog")); + Util::WeatherUI::Checkbox(T(TKEY("enable_volumetric_fog"), "Enable Volumetric Fog"), this, "volumetricFogEnabled", (bool*)&settings.volumetricFogEnabled); + if (settings.volumetricFogEnabled) { + Util::WeatherUI::SliderFloat(T(TKEY("volumetric_view_distance"), "Volumetric View Distance"), this, "volumetricFogDistance", &settings.volumetricFogDistance, 1000.0f, 200000.0f, "%.0f"); + Util::WeatherUI::SliderFloat(T(TKEY("volumetric_start_distance"), "Volumetric Start Distance"), this, "volumetricFogStartDistance", &settings.volumetricFogStartDistance, 0.0f, 20000.0f, "%.0f"); + Util::WeatherUI::SliderFloat(T(TKEY("near_fade_in_distance"), "Near Fade In Distance"), this, "volumetricFogNearFadeInDistance", &settings.volumetricFogNearFadeInDistance, 0.0f, 20000.0f, "%.0f"); + Util::WeatherUI::SliderFloat(T(TKEY("volumetric_extinction_scale"), "Volumetric Extinction Scale"), this, "volumetricFogExtinctionScale", &settings.volumetricFogExtinctionScale, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("volumetric_scattering_distribution"), "Volumetric Scattering Distribution"), this, "volumetricFogScatteringDistribution", &settings.volumetricFogScatteringDistribution, -0.9f, 0.9f, "%.2f"); + Util::WeatherUI::ColorEdit4(T(TKEY("volumetric_albedo"), "Volumetric Albedo"), this, "volumetricFogAlbedo", (float*)&settings.volumetricFogAlbedo); + Util::WeatherUI::ColorEdit4(T(TKEY("volumetric_emissive"), "Volumetric Emissive"), this, "volumetricFogEmissive", (float*)&settings.volumetricFogEmissive); + Util::WeatherUI::SliderFloat(T(TKEY("directional_scattering_intensity"), "Directional Scattering Intensity"), this, "volumetricDirectionalScatteringIntensity", &settings.volumetricDirectionalScatteringIntensity, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("sky_lighting_scattering_intensity"), "Sky Lighting Scattering Intensity"), this, "volumetricSkyLightingIntensity", &settings.volumetricSkyLightingIntensity, 0.0f, 10.0f, "%.2f"); + Util::WeatherUI::SliderFloat(T(TKEY("local_light_scattering_intensity"), "Local Light Scattering Intensity"), this, "volumetricLocalLightScatteringIntensity", &settings.volumetricLocalLightScatteringIntensity, 0.0f, 10.0f, "%.2f"); + if (ImGui::TreeNode(T(TKEY("debug"), "Debug"))) { + uint32_t minGridPixelSize = 4; + uint32_t maxGridPixelSize = 64; + uint32_t minGridSizeZ = 16; + uint32_t maxGridSizeZ = 160; + ImGui::SliderScalar(T(TKEY("grid_pixel_size"), "Grid Pixel Size"), ImGuiDataType_U32, &settings.volumetricGridPixelSize, &minGridPixelSize, &maxGridPixelSize, "%u", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderScalar(T(TKEY("grid_depth_slices"), "Grid Depth Slices"), ImGuiDataType_U32, &settings.volumetricGridSizeZ, &minGridSizeZ, &maxGridSizeZ, "%u", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("directional_shadow_bias"), "Directional Shadow Bias"), &settings.volumetricShadowBias, 0.0f, 0.05f, "%.4f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("depth_distribution_scale"), "Depth Distribution Scale"), &settings.volumetricDepthDistributionScale, 1.0f, 128.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("temporal_history_weight"), "Temporal History Weight"), &settings.volumetricHistoryWeight, 0.0f, 0.99f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + uint32_t minHistoryMissSampleCount = 1; + uint32_t maxHistoryMissSampleCount = 16; + ImGui::SliderScalar(T(TKEY("history_miss_samples"), "History Miss Samples"), ImGuiDataType_U32, &settings.volumetricHistoryMissSampleCount, &minHistoryMissSampleCount, &maxHistoryMissSampleCount, "%u", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat(T(TKEY("sample_jitter_multiplier"), "Sample Jitter Multiplier"), &settings.volumetricSampleJitterMultiplier, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("sample_jitter_multiplier_tooltip"), + "Matches UE's r.VolumetricFog.LightScatteringSampleJitterMultiplier.\n" + "Adds per-voxel random offset on top of the Halton sequence.\n" + "0 = UE default; nonzero values need stronger temporal filtering.")); + } + ImGui::SliderFloat(T(TKEY("upsample_jitter_multiplier"), "Upsample Jitter Multiplier"), &settings.volumetricUpsampleJitterMultiplier, 0.0f, 1.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("upsample_jitter_multiplier_tooltip"), + "Matches UE's r.VolumetricFog.UpsampleJitterMultiplier.\n" + "Jitters the final 3D fog lookup in screen space to hide\n" + "low-resolution froxel pixelization. 0 = UE default.")); + } + ImGui::TreePop(); + } + } +} + +void ExponentialHeightFog::SetupResources() +{ + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.MaxAnisotropy = 1; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + DX::ThrowIfFailed(globals::d3d::device->CreateSamplerState(&samplerDesc, linearSampler.put())); + Util::SetResourceName(linearSampler.get(), "ExponentialHeightFog::LinearSampler"); + + samplerDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL; + DX::ThrowIfFailed(globals::d3d::device->CreateSamplerState(&samplerDesc, shadowSampler.put())); + Util::SetResourceName(shadowSampler.get(), "ExponentialHeightFog::ShadowSampler"); + + volumetricFogCB = std::make_unique(ConstantBufferDesc(), "ExponentialHeightFog::VolumetricFogCB"); +} + +void ExponentialHeightFog::ClearShaderCache() +{ + if (materialSetupCS) { + materialSetupCS->Release(); + materialSetupCS = nullptr; + } + if (conservativeDepthCS) { + conservativeDepthCS->Release(); + conservativeDepthCS = nullptr; + } + if (lightScatteringCS) { + lightScatteringCS->Release(); + lightScatteringCS = nullptr; + } + if (integrationCS) { + integrationCS->Release(); + integrationCS = nullptr; + } +} + +void ExponentialHeightFog::CaptureDirectionalShadowMap() +{ + ID3D11ShaderResourceView* shadowMap = nullptr; + globals::d3d::context->PSGetShaderResources(4, 1, &shadowMap); + directionalShadowMap.copy_from(shadowMap); + if (shadowMap) + shadowMap->Release(); +} + +void ExponentialHeightFog::EnsureVolumetricResources() +{ + uint32_t pixelSize = std::clamp(settings.volumetricGridPixelSize, 4u, 64u); + const uint32_t gridZ = std::clamp(settings.volumetricGridSizeZ, 16u, 160u); + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + + auto getGridSize = [&renderSize, gridZ](uint32_t a_pixelSize) { + return DirectX::XMUINT4{ + std::max(1u, static_cast(std::ceil(renderSize.x / static_cast(a_pixelSize)))), + std::max(1u, static_cast(std::ceil(renderSize.y / static_cast(a_pixelSize)))), + gridZ, + 0u + }; + }; + DirectX::XMUINT4 gridSize = getGridSize(pixelSize); + + constexpr uint64_t maxVolumeVoxels = 16ull * 1024ull * 1024ull; + while (pixelSize < 64u && + static_cast(gridSize.x) * gridSize.y * gridSize.z > maxVolumeVoxels) { + pixelSize++; + gridSize = getGridSize(pixelSize); + } + + if (vBufferA && currentGridSize.x == gridSize.x && currentGridSize.y == gridSize.y && currentGridSize.z == gridSize.z) + return; + + currentGridSize = gridSize; + + D3D11_TEXTURE3D_DESC texDesc{}; + texDesc.Width = gridSize.x; + texDesc.Height = gridSize.y; + texDesc.Depth = gridSize.z; + texDesc.MipLevels = 1; + texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + srvDesc.Format = texDesc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE3D; + srvDesc.Texture3D.MipLevels = 1; + + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc{}; + uavDesc.Format = texDesc.Format; + uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE3D; + uavDesc.Texture3D.MipSlice = 0; + uavDesc.Texture3D.FirstWSlice = 0; + uavDesc.Texture3D.WSize = gridSize.z; + + vBufferA = std::make_unique(texDesc, "ExponentialHeightFog::VBufferA"); + vBufferA->CreateSRV(srvDesc); + vBufferA->CreateUAV(uavDesc); + + D3D11_TEXTURE2D_DESC conservativeDepthDesc{}; + conservativeDepthDesc.Width = gridSize.x; + conservativeDepthDesc.Height = gridSize.y; + conservativeDepthDesc.MipLevels = 1; + conservativeDepthDesc.ArraySize = 1; + conservativeDepthDesc.Format = DXGI_FORMAT_R32_FLOAT; + conservativeDepthDesc.SampleDesc.Count = 1; + conservativeDepthDesc.Usage = D3D11_USAGE_DEFAULT; + conservativeDepthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + + D3D11_SHADER_RESOURCE_VIEW_DESC conservativeDepthSrvDesc{}; + conservativeDepthSrvDesc.Format = conservativeDepthDesc.Format; + conservativeDepthSrvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + conservativeDepthSrvDesc.Texture2D.MipLevels = 1; + + D3D11_UNORDERED_ACCESS_VIEW_DESC conservativeDepthUavDesc{}; + conservativeDepthUavDesc.Format = conservativeDepthDesc.Format; + conservativeDepthUavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; + + conservativeDepth = std::make_unique(conservativeDepthDesc, "ExponentialHeightFog::ConservativeDepth"); + conservativeDepth->CreateSRV(conservativeDepthSrvDesc); + conservativeDepth->CreateUAV(conservativeDepthUavDesc); + + conservativeDepthHistory = std::make_unique(conservativeDepthDesc, "ExponentialHeightFog::ConservativeDepthHistory"); + conservativeDepthHistory->CreateSRV(conservativeDepthSrvDesc); + + lightScattering = std::make_unique(texDesc, "ExponentialHeightFog::LightScattering"); + lightScattering->CreateSRV(srvDesc); + lightScattering->CreateUAV(uavDesc); + + lightScatteringHistory = std::make_unique(texDesc, "ExponentialHeightFog::LightScatteringHistory"); + lightScatteringHistory->CreateSRV(srvDesc); + + integratedLightScattering = std::make_unique(texDesc, "ExponentialHeightFog::IntegratedLightScattering"); + integratedLightScattering->CreateSRV(srvDesc); + integratedLightScattering->CreateUAV(uavDesc); + + hasLightScatteringHistory = false; + hasConservativeDepthHistory = false; + lastPrepassFrame = UINT32_MAX; +} + +void ExponentialHeightFog::ReleaseVolumetricResources() +{ + vBufferA.reset(); + conservativeDepth.reset(); + conservativeDepthHistory.reset(); + lightScattering.reset(); + lightScatteringHistory.reset(); + integratedLightScattering.reset(); + currentGridSize = {}; + hasLightScatteringHistory = false; + hasConservativeDepthHistory = false; + lastPrepassFrame = UINT32_MAX; + ID3D11ShaderResourceView* nullSRV = nullptr; + globals::d3d::context->PSSetShaderResources(19, 1, &nullSRV); +} + +void ExponentialHeightFog::BindIntegratedLightScattering() +{ + ID3D11ShaderResourceView* srv = integratedLightScattering ? integratedLightScattering->srv.get() : nullptr; + globals::d3d::context->PSSetShaderResources(19, 1, &srv); +} + +ID3D11ComputeShader* ExponentialHeightFog::GetMaterialSetupCS() +{ + if (!materialSetupCS) + materialSetupCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ExponentialHeightFog\\VolumetricFogMaterialCS.hlsl", {}, "cs_5_0")); + return materialSetupCS; +} + +ID3D11ComputeShader* ExponentialHeightFog::GetConservativeDepthCS() +{ + if (!conservativeDepthCS) + conservativeDepthCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ExponentialHeightFog\\VolumetricFogConservativeDepthCS.hlsl", {}, "cs_5_0")); + return conservativeDepthCS; +} + +ID3D11ComputeShader* ExponentialHeightFog::GetLightScatteringCS() +{ + if (!lightScatteringCS) { + std::vector> defines; + if (globals::features::lightLimitFix.loaded) { + defines.emplace_back("LIGHT_LIMIT_FIX", ""); + } + if (globals::features::terrainShadows.loaded) { + defines.emplace_back("TERRAIN_SHADOWS", ""); + } + if (globals::features::cloudShadows.loaded) { + defines.emplace_back("CLOUD_SHADOWS", ""); + } + lightScatteringCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ExponentialHeightFog\\VolumetricFogLightScatteringCS.hlsl", defines, "cs_5_0")); + } + return lightScatteringCS; +} + +ID3D11ComputeShader* ExponentialHeightFog::GetIntegrationCS() +{ + if (!integrationCS) + integrationCS = static_cast(Util::CompileShader(L"Data\\Shaders\\ExponentialHeightFog\\VolumetricFogIntegrationCS.hlsl", {}, "cs_5_0")); + return integrationCS; +} + +void ExponentialHeightFog::Prepass() +{ + if (!settings.enabled || !settings.volumetricFogEnabled || settings.volumetricFogExtinctionScale <= 0.0f) { + ReleaseVolumetricResources(); + return; + } + + EnsureVolumetricResources(); + + if (settings.fogDensity <= 0.0f) { + hasLightScatteringHistory = false; + hasConservativeDepthHistory = false; + lastPrepassFrame = UINT32_MAX; + BindIntegratedLightScattering(); + return; + } + + ID3D11ShaderResourceView* directionalShadowLightData = globals::deferred && globals::deferred->directionalShadowLights ? globals::deferred->directionalShadowLights->srv.get() : nullptr; + auto& lightLimitFix = globals::features::lightLimitFix; + const bool hasLocalLightData = + lightLimitFix.loaded && + lightLimitFix.lights && + lightLimitFix.lightIndexList && + lightLimitFix.lightGrid; + auto* depthSrv = Util::GetCurrentSceneDepthSRV(true); + auto& ibl = globals::features::ibl; + auto& skylighting = globals::features::skylighting; + const bool hasIBL = ibl.loaded && + ibl.settings.EnableIBL != 0 && + !(ibl.settings.DisableInInteriors && Util::IsInterior()) && + ibl.envIBLTexture && + ibl.skyIBLTexture; + const bool hasSkylighting = skylighting.loaded && skylighting.texProbeArray; + + const bool temporalReprojection = Util::GetTemporal(); + const bool temporalHistoryValid = + temporalReprojection && + hasLightScatteringHistory && + lastPrepassFrame != UINT32_MAX && + globals::state->frameCount == lastPrepassFrame + 1u; + + VolumetricFogCB cb{}; + cb.gridSizeAndFlags = { + currentGridSize.x, + currentGridSize.y, + currentGridSize.z, + (directionalShadowMap && directionalShadowLightData ? 1u : 0u) | + (depthSrv ? 2u : 0u) | + (hasIBL ? 4u : 0u) | + (hasSkylighting ? 8u : 0u) | + (depthSrv && temporalHistoryValid && hasConservativeDepthHistory ? 16u : 0u) | + (hasLocalLightData ? 32u : 0u) + }; + cb.invGridSizeAndNearFade = { + 1.0f / static_cast(currentGridSize.x), + 1.0f / static_cast(currentGridSize.y), + 1.0f / static_cast(currentGridSize.z), + settings.volumetricFogNearFadeInDistance > 0.0f ? 1.0f / settings.volumetricFogNearFadeInDistance : 100000000.0f + }; + + const auto cameraData = Util::GetCameraData(); + const double nearPlane = std::max(static_cast(cameraData.y), static_cast(std::max(settings.volumetricFogStartDistance, 0.0f))); + const double farPlane = std::max(nearPlane + 1.0, static_cast(std::max(settings.volumetricFogDistance, settings.volumetricFogStartDistance + 1.0f))); + const double nearWithOffset = nearPlane + 0.095 * 100.0; + const double depthDistributionScale = std::max( + static_cast(settings.volumetricDepthDistributionScale), + static_cast(currentGridSize.z) / 120.0); + const double farExp = std::exp2(std::min(static_cast(currentGridSize.z) / depthDistributionScale, 120.0)); + const double gridZOffset = (farPlane - nearWithOffset * farExp) / (farPlane - nearWithOffset); + const double gridZScale = (1.0 - gridZOffset) / nearWithOffset; + cb.gridZParams = { + static_cast(gridZScale), + static_cast(gridZOffset), + static_cast(depthDistributionScale), + 0.0f + }; + + const uint32_t eyeCount = globals::game::isVR ? 2u : 1u; + for (uint32_t eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { + cb.clipToWorld[eyeIndex] = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Invert(); + } + if (eyeCount == 1u) { + cb.clipToWorld[1] = cb.clipToWorld[0]; + } + + for (uint32_t i = 0; i < std::size(cb.frameJitterOffsets); i++) { + const uint32_t temporalFrame = (globals::state->frameCount - i) & 1023u; + cb.frameJitterOffsets[i] = { + temporalReprojection ? Halton(temporalFrame, 2) : 0.5f, + temporalReprojection ? Halton(temporalFrame, 3) : 0.5f, + temporalReprojection ? Halton(temporalFrame, 5) : 0.5f, + 0.0f + }; + } + cb.historyParameters = { + temporalHistoryValid ? std::clamp(settings.volumetricHistoryWeight, 0.0f, 0.99f) : 0.0f, + static_cast(std::clamp(settings.volumetricHistoryMissSampleCount, 1u, 16u)), + 0.0f, + 0.0f + }; + cb.jitterParameters = { + temporalReprojection ? std::max(settings.volumetricSampleJitterMultiplier, 0.0f) : 0.0f, + static_cast(globals::state->frameCount % 8u), + 0.0f, + 0.0f + }; + volumetricFogCB->Update(cb); + + auto context = globals::d3d::context; + ID3D11Buffer* cbuffers[1]{ volumetricFogCB->CB() }; + context->CSSetConstantBuffers(0, 1, cbuffers); + + ID3D11Buffer* sharedBuffers[2]{ globals::state->sharedDataCB->CB(), globals::state->featureDataCB->CB() }; + context->CSSetConstantBuffers(5, 2, sharedBuffers); + + ID3D11Buffer* frameBuffers[1]{ *globals::game::perFrame.get() }; + context->CSSetConstantBuffers(12, 1, frameBuffers); + + ID3D11SamplerState* samplers[2]{ linearSampler.get(), shadowSampler.get() }; + context->CSSetSamplers(0, 2, samplers); + + const uint32_t groupX = (currentGridSize.x + 7) / 8; + const uint32_t groupY = (currentGridSize.y + 7) / 8; + const uint32_t groupZ = (currentGridSize.z + 3) / 4; + + context->CSSetShaderResources(17, 1, &depthSrv); + ID3D11ShaderResourceView* skylightingSrv = hasSkylighting ? skylighting.texProbeArray->srv.get() : nullptr; + ID3D11ShaderResourceView* iblSrvs[2]{ + hasIBL ? ibl.envIBLTexture->srv.get() : nullptr, + hasIBL ? ibl.skyIBLTexture->srv.get() : nullptr + }; + context->CSSetShaderResources(50, 1, &skylightingSrv); + context->CSSetShaderResources(76, 2, iblSrvs); + + if (depthSrv) { + ID3D11UnorderedAccessView* uavs[1]{ conservativeDepth->uav.get() }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(GetConservativeDepthCS(), nullptr, 0); + context->Dispatch(groupX, groupY, 1); + uavs[0] = nullptr; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + } + + { + ID3D11UnorderedAccessView* uavs[1]{ vBufferA->uav.get() }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(GetMaterialSetupCS(), nullptr, 0); + context->Dispatch(groupX, groupY, groupZ); + uavs[0] = nullptr; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + } + + { + ID3D11ShaderResourceView* srvs[5]{ + vBufferA->srv.get(), + directionalShadowMap.get(), + temporalHistoryValid ? lightScatteringHistory->srv.get() : nullptr, + conservativeDepth->srv.get(), + temporalHistoryValid && hasConservativeDepthHistory ? conservativeDepthHistory->srv.get() : nullptr + }; + ID3D11ShaderResourceView* localLightSrvs[3]{ + hasLocalLightData ? lightLimitFix.lights->srv.get() : nullptr, + hasLocalLightData ? lightLimitFix.lightIndexList->srv.get() : nullptr, + hasLocalLightData ? lightLimitFix.lightGrid->srv.get() : nullptr + }; + ID3D11UnorderedAccessView* uavs[1]{ lightScattering->uav.get() }; + context->CSSetShaderResources(0, 5, srvs); + context->CSSetShaderResources(35, 3, localLightSrvs); + context->CSSetShaderResources(98, 1, &directionalShadowLightData); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(GetLightScatteringCS(), nullptr, 0); + context->Dispatch(groupX, groupY, groupZ); + uavs[0] = nullptr; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + } + + { + ID3D11ShaderResourceView* srvs[1]{ lightScattering->srv.get() }; + ID3D11UnorderedAccessView* uavs[1]{ integratedLightScattering->uav.get() }; + context->CSSetShaderResources(0, 1, srvs); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(GetIntegrationCS(), nullptr, 0); + context->Dispatch(groupX, groupY, 1); + } + + ID3D11ShaderResourceView* nullSrvs[5]{ nullptr, nullptr, nullptr, nullptr, nullptr }; + ID3D11ShaderResourceView* nullDepthSrv[1]{ nullptr }; + ID3D11UnorderedAccessView* nullUav[1]{ nullptr }; + ID3D11SamplerState* nullSamplers[2]{ nullptr, nullptr }; + ID3D11Buffer* nullCb[1]{ nullptr }; + context->CSSetShaderResources(0, 5, nullSrvs); + context->CSSetShaderResources(17, 1, nullDepthSrv); + context->CSSetShaderResources(35, 3, nullSrvs); + context->CSSetShaderResources(50, 1, nullDepthSrv); + context->CSSetShaderResources(76, 2, nullSrvs); + context->CSSetShaderResources(98, 1, nullSrvs); + context->CSSetUnorderedAccessViews(0, 1, nullUav, nullptr); + context->CSSetSamplers(0, 2, nullSamplers); + context->CSSetConstantBuffers(0, 1, nullCb); + context->CSSetShader(nullptr, nullptr, 0); + + if (temporalReprojection) { + context->CopyResource(lightScatteringHistory->resource.get(), lightScattering->resource.get()); + hasLightScatteringHistory = true; + if (depthSrv) { + context->CopyResource(conservativeDepthHistory->resource.get(), conservativeDepth->resource.get()); + hasConservativeDepthHistory = true; + } else { + hasConservativeDepthHistory = false; + } + } else { + hasLightScatteringHistory = false; + hasConservativeDepthHistory = false; + } + + lastPrepassFrame = globals::state->frameCount; + BindIntegratedLightScattering(); } void ExponentialHeightFog::RegisterWeatherVariables() @@ -140,7 +653,7 @@ void ExponentialHeightFog::RegisterWeatherVariables() "directionalInscatteringAnisotropy", "Henyey-Greenstein asymmetry parameter. Positive = forward scattering, 0 = isotropic, negative = back scattering.", &settings.directionalInscatteringAnisotropy, - 0.7f, + 0.2f, -0.99f, 0.99f)); registry->RegisterVariable(std::make_shared( @@ -169,5 +682,93 @@ void ExponentialHeightFog::RegisterWeatherVariables() [](const bool& from, const bool& to, float factor) { return factor > 0.5f ? to : from; })); + + registry->RegisterVariable(std::make_shared>( + "volumetricFogEnabled", + "Enable Volumetric Fog", + "Enables froxel-based volumetric fog for exponential height fog", + (bool*)&settings.volumetricFogEnabled, + false, + [](const bool& from, const bool& to, float factor) { + return factor > 0.5f ? to : from; + })); + + registry->RegisterVariable(std::make_shared( + "Volumetric View Distance", + "volumetricFogDistance", + "Maximum distance covered by exponential height volumetric fog", + &settings.volumetricFogDistance, + 60000.0f, + 1000.0f, 200000.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Start Distance", + "volumetricFogStartDistance", + "Start distance of volumetric fog from the camera", + &settings.volumetricFogStartDistance, + 0.0f, + 0.0f, 200000.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Near Fade In Distance", + "volumetricFogNearFadeInDistance", + "Distance over which volumetric fog fades in near the camera", + &settings.volumetricFogNearFadeInDistance, + 1000.0f, + 0.0f, 20000.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Extinction Scale", + "volumetricFogExtinctionScale", + "Scale applied to volumetric fog extinction", + &settings.volumetricFogExtinctionScale, + 1.0f, + 0.0f, 10.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Scattering Distribution", + "volumetricFogScatteringDistribution", + "Henyey-Greenstein scattering distribution for volumetric fog", + &settings.volumetricFogScatteringDistribution, + 0.2f, + -0.9f, 0.9f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Directional Scattering Intensity", + "volumetricDirectionalScatteringIntensity", + "Scale applied to volumetric fog directional light scattering", + &settings.volumetricDirectionalScatteringIntensity, + 1.0f, + 0.0f, 10.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Albedo", + "volumetricFogAlbedo", + "Volumetric fog albedo color", + &settings.volumetricFogAlbedo, + float4{ 1.0f, 1.0f, 1.0f, 1.0f })); + + registry->RegisterVariable(std::make_shared( + "Volumetric Emissive", + "volumetricFogEmissive", + "Volumetric fog emissive color", + &settings.volumetricFogEmissive, + float4{ 0.0f, 0.0f, 0.0f, 0.0f })); + + registry->RegisterVariable(std::make_shared( + "Volumetric Sky Lighting Intensity", + "volumetricSkyLightingIntensity", + "Scale applied to volumetric fog sky lighting", + &settings.volumetricSkyLightingIntensity, + 1.0f, + 0.0f, 10.0f)); + + registry->RegisterVariable(std::make_shared( + "Volumetric Local Light Scattering Intensity", + "volumetricLocalLightScatteringIntensity", + "Scale applied to volumetric fog local light scattering", + &settings.volumetricLocalLightScatteringIntensity, + 1.0f, + 0.0f, 100.0f)); } #undef I18N_KEY_PREFIX diff --git a/src/Features/ExponentialHeightFog.h b/src/Features/ExponentialHeightFog.h index f9e41e99cf..b386df78e5 100644 --- a/src/Features/ExponentialHeightFog.h +++ b/src/Features/ExponentialHeightFog.h @@ -1,5 +1,7 @@ #pragma once +#include "Buffer.h" + struct ExponentialHeightFog : Feature { private: @@ -25,31 +27,94 @@ struct ExponentialHeightFog : Feature bool HasShaderDefine(RE::BSShader::Type) override { return true; }; virtual void DrawSettings() override; + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + virtual void Prepass() override; virtual void RestoreDefaultSettings() override; virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; void RegisterWeatherVariables() override; + void CaptureDirectionalShadowMap(); - struct alignas(16) Settings + struct Settings { uint enabled = 0; - uint useDynamicCubemaps = 0; + uint useDynamicCubemaps = 1; float startDistance = 0.0f; float fogHeight = 0.0f; float fogHeightFalloff = 0.2f; - float fogDensity = 0.02f; + float fogDensity = 0.005f; float directionalInscatteringMultiplier = 1.0f; - float directionalInscatteringAnisotropy = 0.7f; + float directionalInscatteringAnisotropy = 0.2f; float4 inscatteringTint = { 1.0f, 1.0f, 1.0f, 1.0f }; - float cubemapMipLevel = 3.0f; + float cubemapMipLevel = 7.0f; float sunlightAttenuationAmount = 1.0f; uint respectVanillaFogFade = 0; - uint disableVanillaFog = 0; + uint disableVanillaFog = 1; float4 fogInscatteringColor = { 0.0f, 0.0f, 0.0f, 1.0f }; - float originalFogColorAmount = 1.0f; - float3 pad; + float originalFogColorAmount = 0.0f; + uint volumetricFogEnabled = 0; + uint volumetricGridPixelSize = 16; + uint volumetricGridSizeZ = 64; + float volumetricFogDistance = 60000.0f; + float volumetricFogStartDistance = 0.0f; + float volumetricFogNearFadeInDistance = 1000.0f; + float volumetricFogExtinctionScale = 1.0f; + float4 volumetricFogAlbedo = { 1.0f, 1.0f, 1.0f, 1.0f }; + float4 volumetricFogEmissive = { 0.0f, 0.0f, 0.0f, 0.0f }; + float volumetricDirectionalScatteringIntensity = 1.0f; + float volumetricShadowBias = 0.002f; + float volumetricDepthDistributionScale = 8.0f; + float volumetricSkyLightingIntensity = 1.0f; + float volumetricFogScatteringDistribution = 0.2f; + float volumetricHistoryWeight = 0.96f; + uint volumetricHistoryMissSampleCount = 4; + float volumetricSampleJitterMultiplier = 0.0f; + float volumetricUpsampleJitterMultiplier = 1.0f; + float volumetricLocalLightScatteringIntensity = 1.0f; + float2 pad0; } settings; - static_assert(sizeof(Settings) == sizeof(float4) * 6, "Settings must match HLSL ExponentialHeightFogSettings."); + STATIC_ASSERT_ALIGNAS_16(Settings); + +private: + struct VolumetricFogCB + { + DirectX::XMUINT4 gridSizeAndFlags = {}; + float4 invGridSizeAndNearFade = {}; + float4 gridZParams = {}; + float4x4 clipToWorld[2] = {}; + float4 frameJitterOffsets[16] = {}; + float4 historyParameters = {}; + float4 jitterParameters = {}; // x = LightScatteringSampleJitterMultiplier, y = StateFrameIndexMod8, zw = unused + }; + STATIC_ASSERT_ALIGNAS_16(VolumetricFogCB); + + void EnsureVolumetricResources(); + void ReleaseVolumetricResources(); + void BindIntegratedLightScattering(); + ID3D11ComputeShader* GetMaterialSetupCS(); + ID3D11ComputeShader* GetConservativeDepthCS(); + ID3D11ComputeShader* GetLightScatteringCS(); + ID3D11ComputeShader* GetIntegrationCS(); + + std::unique_ptr vBufferA; + std::unique_ptr conservativeDepth; + std::unique_ptr conservativeDepthHistory; + std::unique_ptr lightScattering; + std::unique_ptr lightScatteringHistory; + std::unique_ptr integratedLightScattering; + std::unique_ptr volumetricFogCB; + winrt::com_ptr linearSampler; + winrt::com_ptr shadowSampler; + winrt::com_ptr directionalShadowMap; + ID3D11ComputeShader* materialSetupCS = nullptr; + ID3D11ComputeShader* conservativeDepthCS = nullptr; + ID3D11ComputeShader* lightScatteringCS = nullptr; + ID3D11ComputeShader* integrationCS = nullptr; + DirectX::XMUINT4 currentGridSize = {}; + bool hasLightScatteringHistory = false; + bool hasConservativeDepthHistory = false; + uint32_t lastPrepassFrame = UINT32_MAX; }; diff --git a/src/State.cpp b/src/State.cpp index 9bc07e59fd..bf50000151 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -8,6 +8,7 @@ #include "FeatureIssues.h" #include "Features/CSEditor.h" #include "Features/CloudShadows.h" +#include "Features/ExponentialHeightFog.h" #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" @@ -103,6 +104,8 @@ void State::Draw() if (currentPixelDescriptor & static_cast(SIE::ShaderCache::UtilityShaderFlags::RenderShadowmask)) { if (volumetricShadows.loaded) volumetricShadows.CopyShadowLightData(); + if (globals::features::exponentialHeightFog.loaded) + globals::features::exponentialHeightFog.CaptureDirectionalShadowMap(); } } } From c185b5c120a3eb39f1a670bc01a66d7aa1a1dd86 Mon Sep 17 00:00:00 2001 From: Dlizzio <77717521+Dlizzio@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:15:11 -0700 Subject: [PATCH 19/55] fix(UI): DPI scale new UI additions (#2457) --- .../CommunityShaders/Translations/en.json | 10 ++ src/Features/HDRDisplay.cpp | 16 ++- src/Menu/FeatureListRenderer.cpp | 2 +- src/Menu/ProfilingRenderer.cpp | 135 ++++++++++++------ src/Menu/ProfilingRenderer.h | 2 + src/Menu/SettingsTabRenderer.cpp | 28 ++-- src/Menu/ThemeManager.h | 12 +- src/Utils/LegitProfiler.h | 41 +++--- src/Utils/UI.cpp | 24 ++-- src/Utils/UI.h | 15 +- 10 files changed, 181 insertions(+), 104 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 36de2bcfe3..7d122ef376 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1853,6 +1853,16 @@ "menu.issues.warning_label": "WARNING:", "menu.issues.wrong_version_desc": "The following features have version compatibility issues and were disabled automatically. Please check for any updates or if the feature is considered obsolete.", "menu.issues.wrong_version_header": "Wrong Version Features", + "menu.profiling.avg": "Avg", + "menu.profiling.cpu": "CPU", + "menu.profiling.gpu": "GPU", + "menu.profiling.no_timing_data": "No timing data", + "menu.profiling.no_timing_data_world": "No timing data available (enter game world)", + "menu.profiling.p95": "P95", + "menu.profiling.p99": "P99", + "menu.profiling.pass": "Pass", + "menu.profiling.percent": "%", + "menu.profiling.total": "Total", "menu.restore_settings": "Restore Saved Settings", "menu.save_settings": "Save Settings", "menu.settings.auto_hide_feature_list": "Auto-hide Feature List", diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 8a944c32ee..086129f98e 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -11,6 +11,7 @@ #include "State.h" #include "Upscaling.h" #include "Util.h" +#include #include #include #include @@ -424,7 +425,18 @@ void HDRDisplay::DrawSettings() ImGui::Separator(); ImGui::Spacing(); - if (ImGui::Button(T(TKEY("force_enable_hdr"), "Force Enable HDR"), ImVec2(150, 0))) { + const auto buttonWidthForLabel = [](const char* label) { + return ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2.0f; + }; + const char* forceEnableLabel = T(TKEY("force_enable_hdr"), "Force Enable HDR"); + const char* cancelLabel = T(TKEY("cancel"), "Cancel"); + const float buttonWidth = std::max({ + ThemeManager::Constants::POPUP_BUTTON_WIDTH * Util::GetUIScale(), + buttonWidthForLabel(forceEnableLabel), + buttonWidthForLabel(cancelLabel) + }); + + if (ImGui::Button(forceEnableLabel, ImVec2(buttonWidth, 0))) { { std::lock_guard lock(settingsMutex); settings.enableHDR = true; @@ -437,7 +449,7 @@ void HDRDisplay::DrawSettings() ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button(T(TKEY("cancel"), "Cancel"), ImVec2(150, 0))) { + if (ImGui::Button(cancelLabel, ImVec2(buttonWidth, 0))) { { std::lock_guard lock(settingsMutex); settings.enableHDR = false; diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index f1483611d3..c77adda583 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -813,7 +813,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, feat->DrawSettings(); if (feat != &globals::features::csEditor) { - ImGui::SeparatorText("Profiling"); + ImGui::SeparatorText(T("menu.features.profiling", "Profiling")); ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); } diff --git a/src/Menu/ProfilingRenderer.cpp b/src/Menu/ProfilingRenderer.cpp index 832fddb81b..67912426de 100644 --- a/src/Menu/ProfilingRenderer.cpp +++ b/src/Menu/ProfilingRenderer.cpp @@ -6,7 +6,10 @@ #include #include "Globals.h" +#include "I18n/I18n.h" +#include "Menu.h" #include "State.h" +#include "Utils/UI.h" static ImU32 HslToImU32(float h, float s, float l) { @@ -38,6 +41,43 @@ static ImU32 HslToImU32(float h, float s, float l) } static constexpr float kGoldenRatio = 0.618033988749895f; +static constexpr float kGraphHeadroomScale = 1.2f; +static constexpr float kMainGraphLegendWidth = 260.0f; +static constexpr float kFeatureGraphLegendWidth = 200.0f; +static constexpr float kMinGraphWidth = 100.0f; +static constexpr float kMainGraphHeight = 180.0f; +static constexpr float kFeatureGraphHeight = 100.0f; +static constexpr float kMainGraphMinFrameTimeSec = 0.0001f; +static constexpr float kFeatureGraphMinFrameTimeSec = 0.00001f; +static constexpr float kTimingTableMetricColumnWidth = 55.0f; +static constexpr float kTimingTablePercentColumnWidth = 45.0f; +static constexpr float kStatsRefreshSeconds = 1.0f; + +struct GraphLayout +{ + float graphWidth; + float legendWidth; + float height; + float uiScale; +}; + +static GraphLayout GetGraphLayout(float availableWidth, float baseLegendWidth, float baseHeight) +{ + const float uiScale = Util::GetUIScale(); + const float contentWidth = std::max(0.0f, availableWidth); + const float minGraphWidth = kMinGraphWidth * uiScale; + const float desiredLegendWidth = baseLegendWidth * uiScale; + const float legendWidth = contentWidth > minGraphWidth ? + std::min(desiredLegendWidth, contentWidth - minGraphWidth) : + 0.0f; + + return { + contentWidth - legendWidth, + legendWidth, + baseHeight * uiScale, + uiScale + }; +} ImU32 ProfilingRenderer::GetGroupColor(const std::string& groupName) { @@ -88,6 +128,34 @@ void ProfilingRenderer::TextHeat(const char* fmt, float value, float maxValue) ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), fmt, value); } +void ProfilingRenderer::RenderTimingModeToggle() +{ + int mode = static_cast(timingMode); + + ImGui::PushID("ProfilingTimingMode"); + ImGui::RadioButton(T("menu.profiling.gpu", "GPU"), &mode, static_cast(TimingMode::GPU)); + ImGui::SameLine(); + ImGui::RadioButton(T("menu.profiling.cpu", "CPU"), &mode, static_cast(TimingMode::CPU)); + ImGui::PopID(); + + const auto newMode = static_cast(mode); + if (newMode != timingMode) { + timingMode = newMode; + timeSinceLastUpdate = kStatsRefreshSeconds; + } +} + +void ProfilingRenderer::SetupTimingTableColumns(bool includePercentColumn) +{ + const float scale = Util::GetUIScale(); + ImGui::TableSetupColumn(T("menu.profiling.pass", "Pass"), ImGuiTableColumnFlags_WidthStretch, 3.0f); + ImGui::TableSetupColumn(T("menu.profiling.avg", "Avg"), ImGuiTableColumnFlags_WidthFixed, kTimingTableMetricColumnWidth * scale); + ImGui::TableSetupColumn(T("menu.profiling.p95", "P95"), ImGuiTableColumnFlags_WidthFixed, kTimingTableMetricColumnWidth * scale); + ImGui::TableSetupColumn(T("menu.profiling.p99", "P99"), ImGuiTableColumnFlags_WidthFixed, kTimingTableMetricColumnWidth * scale); + if (includePercentColumn) + ImGui::TableSetupColumn(T("menu.profiling.percent", "%"), ImGuiTableColumnFlags_WidthFixed, kTimingTablePercentColumnWidth * scale); +} + void ProfilingRenderer::RenderGraph() { auto& profiler = (*globals::profiler); @@ -127,16 +195,13 @@ void ProfilingRenderer::RenderGraph() gpuGraph.LoadFrameData(tasks.data(), tasks.size()); - float maxFrameTimeSec = gpuGraph.GetPeakFrameTime() * 1.2f; - if (maxFrameTimeSec < 0.0001f) - maxFrameTimeSec = 0.0001f; + float maxFrameTimeSec = gpuGraph.GetPeakFrameTime() * kGraphHeadroomScale; + if (maxFrameTimeSec < kMainGraphMinFrameTimeSec) + maxFrameTimeSec = kMainGraphMinFrameTimeSec; - float availWidth = ImGui::GetContentRegionAvail().x; - int legendWidth = 260; - int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); - int graphHeight = 180; + const auto layout = GetGraphLayout(ImGui::GetContentRegionAvail().x, kMainGraphLegendWidth, kMainGraphHeight); - gpuGraph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + gpuGraph.RenderTimings(layout.graphWidth, layout.legendWidth, layout.height, 0, maxFrameTimeSec, layout.uiScale); ImGui::Spacing(); } @@ -147,14 +212,7 @@ void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) bool cpuMode = (timingMode == TimingMode::CPU); if (showModeToggle) { - int mode = static_cast(timingMode); - ImGui::RadioButton("GPU", &mode, 0); - ImGui::SameLine(); - ImGui::RadioButton("CPU", &mode, 1); - if (static_cast(mode) != timingMode) { - timingMode = static_cast(mode); - timeSinceLastUpdate = 1.0f; - } + RenderTimingModeToggle(); cpuMode = (timingMode == TimingMode::CPU); ImGui::Separator(); } @@ -164,7 +222,7 @@ void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) lastFrameTime = currentTime; timeSinceLastUpdate += deltaTime; - if (timeSinceLastUpdate >= 1.0f) { + if (timeSinceLastUpdate >= kStatsRefreshSeconds) { timeSinceLastUpdate = 0.0f; cachedGroups.clear(); @@ -218,7 +276,7 @@ void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) } if (cachedGroups.empty()) { - ImGui::TextDisabled("No timing data available (enter game world)"); + ImGui::TextDisabled("%s", T("menu.profiling.no_timing_data_world", "No timing data available (enter game world)")); return; } @@ -231,11 +289,7 @@ void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX | ImGuiTableFlags_ScrollY, ImVec2(0.0f, availHeight))) { ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); - ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); - ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); - ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); - ImGui::TableSetupColumn("%%", ImGuiTableColumnFlags_WidthFixed, 45.0f); + SetupTimingTableColumns(true); ImGui::TableHeadersRow(); for (const auto& group : cachedGroups) { @@ -294,11 +348,7 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) auto& profiler = (*globals::profiler); const auto& results = profiler.GetResults(); - int mode = static_cast(timingMode); - ImGui::RadioButton("GPU", &mode, 0); - ImGui::SameLine(); - ImGui::RadioButton("CPU", &mode, 1); - timingMode = static_cast(mode); + RenderTimingModeToggle(); bool cpuMode = (timingMode == TimingMode::CPU); @@ -340,7 +390,7 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) } if (entries.empty()) { - ImGui::TextDisabled("No timing data"); + ImGui::TextDisabled("%s", T("menu.profiling.no_timing_data", "No timing data")); return; } @@ -361,24 +411,18 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) if (!tasks.empty()) { state.graph.LoadFrameData(tasks.data(), tasks.size()); - float maxFrameTimeSec = state.graph.GetPeakFrameTime() * 1.2f; - if (maxFrameTimeSec < 0.00001f) - maxFrameTimeSec = 0.00001f; + float maxFrameTimeSec = state.graph.GetPeakFrameTime() * kGraphHeadroomScale; + if (maxFrameTimeSec < kFeatureGraphMinFrameTimeSec) + maxFrameTimeSec = kFeatureGraphMinFrameTimeSec; - float availWidth = ImGui::GetContentRegionAvail().x; - int legendWidth = 200; - int graphWidth = std::max(100, static_cast(availWidth) - legendWidth); - int graphHeight = 100; + const auto layout = GetGraphLayout(ImGui::GetContentRegionAvail().x, kFeatureGraphLegendWidth, kFeatureGraphHeight); - state.graph.RenderTimings(graphWidth, legendWidth, graphHeight, 0, maxFrameTimeSec); + state.graph.RenderTimings(layout.graphWidth, layout.legendWidth, layout.height, 0, maxFrameTimeSec, layout.uiScale); ImGui::Spacing(); } if (ImGui::BeginTable("##FeatureTimers", 4, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX)) { - ImGui::TableSetupColumn("Pass", ImGuiTableColumnFlags_WidthStretch, 3.0f); - ImGui::TableSetupColumn("Avg", ImGuiTableColumnFlags_WidthFixed, 55.0f); - ImGui::TableSetupColumn("P95", ImGuiTableColumnFlags_WidthFixed, 55.0f); - ImGui::TableSetupColumn("P99", ImGuiTableColumnFlags_WidthFixed, 55.0f); + SetupTimingTableColumns(false); ImGui::TableHeadersRow(); for (const auto& e : entries) { @@ -395,13 +439,14 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "Total"); + const auto totalColor = globals::menu->GetTheme().StatusPalette.InfoColor; + ImGui::TextColored(totalColor, "%s", T("menu.profiling.total", "Total")); ImGui::TableNextColumn(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalAvg); + ImGui::TextColored(totalColor, "%.3f", totalAvg); ImGui::TableNextColumn(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP95); + ImGui::TextColored(totalColor, "%.3f", totalP95); ImGui::TableNextColumn(); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.6f, 1.0f), "%.3f", totalP99); + ImGui::TextColored(totalColor, "%.3f", totalP99); ImGui::EndTable(); } diff --git a/src/Menu/ProfilingRenderer.h b/src/Menu/ProfilingRenderer.h index 11d11ac852..9b8be57c97 100644 --- a/src/Menu/ProfilingRenderer.h +++ b/src/Menu/ProfilingRenderer.h @@ -64,5 +64,7 @@ class ProfilingRenderer static uint32_t ToLegitColor(ImU32 imColor); static ImVec4 HeatColor(float value, float maxValue); static void TextHeat(const char* fmt, float value, float maxValue); + static void RenderTimingModeToggle(); + static void SetupTimingTableColumns(bool includePercentColumn); static void RenderGraph(); }; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index b5bce28b15..79b2ae774a 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1208,33 +1208,21 @@ void SettingsTabRenderer::RenderColorsTab() // Color filter at the top with search icon static ImGuiTextFilter colorFilter; - float iconSize = 20.0f; - float iconSpace = iconSize + 14.0f; - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + const float scale = Util::GetSearchUIScale(); + const float iconSize = ThemeManager::Constants::SEARCH_ICON_SIZE * scale; + const float iconSpace = iconSize + ThemeManager::Constants::SEARCH_INPUT_PADDING_EXTRA * scale; float availableWidth = ImGui::GetFontSize() * 16; - float frameHeight = ImGui::GetFrameHeight(); // Custom style for filter with icon space - float scale = Util::GetUIScale(); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f * scale)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, ThemeManager::Constants::SEARCH_INPUT_FRAME_PADDING_Y * scale)); colorFilter.Draw(T("menu.settings.filter_colors", "Filter colors"), availableWidth); ImGui::PopStyleVar(); // Draw search icon - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f * scale, cursorPos.y + (frameHeight - iconSize) * 0.5f); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 center = ImVec2(iconPos.x + iconSize * 0.46f, iconPos.y + iconSize * 0.5f); - float radius = iconSize * 0.3f; - - auto& palette = globals::menu->GetTheme().Palette; - ImVec4 iconColor = palette.Text; - iconColor.w *= 0.7f; - ImU32 iconColorU32 = ImGui::GetColorU32(iconColor); - - drawList->AddCircle(center, radius, iconColorU32, 12, 2.2f); - ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); - ImVec2 handleEnd = ImVec2(handleStart.x + iconSize * 0.29f, handleStart.y + iconSize * 0.29f); - drawList->AddLine(handleStart, handleEnd, iconColorU32, 2.1f); + const ImVec2 filterMin = ImGui::GetItemRectMin(); + const ImVec2 filterSize = ImGui::GetItemRectSize(); + ImVec2 iconPos = ImVec2(filterMin.x + ThemeManager::Constants::SEARCH_ICON_OFFSET_X * scale, filterMin.y + (filterSize.y - iconSize) * 0.5f); + Util::DrawSearchIcon(iconPos, iconSize, ThemeManager::Constants::SEARCH_ICON_ALPHA); ImGui::Spacing(); diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index ea9ed43208..856e9d51c8 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -199,7 +199,15 @@ class ThemeManager static constexpr float SCENE_SETTING_DROPDOWN_RATIO = 0.6f; // Setting dropdown width ratio static constexpr float SCENE_VALUE_LABEL_OFFSET_RATIO = 0.5f; // Value label right-alignment ratio - // Combo search input constants + // Search input constants + static constexpr float SEARCH_BASELINE_SCREEN_HEIGHT = 1440.0f; // Search chrome is authored for 2K. + static constexpr float SEARCH_ICON_SIZE = 20.0f; // Default search icon size + static constexpr float SEARCH_ICON_ALPHA = 0.7f; // Default search icon opacity + static constexpr float SEARCH_ICON_OFFSET_X = 8.0f; // Search icon offset from input edge + static constexpr float SEARCH_INPUT_PADDING_EXTRA = 14.0f; // Extra input padding after icon + static constexpr float SEARCH_INPUT_FRAME_PADDING_Y = 6.0f; // Search input vertical padding + static constexpr float SEARCH_ICON_STROKE_RATIO = 0.11f; // Search icon stroke thickness relative to size + static constexpr float SEARCH_ICON_HANDLE_STROKE_RATIO = 0.105f; static constexpr float COMBO_SEARCH_ICON_SIZE = 16.0f; // Icon size for search inside combos static constexpr float COMBO_SEARCH_ICON_ALPHA = 0.5f; // Icon alpha for subtle appearance static constexpr float COMBO_SEARCH_ICON_OFFSET_X = 5.0f; // Icon horizontal offset from input edge @@ -323,4 +331,4 @@ class ThemeManager // Constants static constexpr size_t MAX_THEMES = 100; // Prevent excessive theme loading static constexpr size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB max theme file size -}; \ No newline at end of file +}; diff --git a/src/Utils/LegitProfiler.h b/src/Utils/LegitProfiler.h index 1a766f88da..837fdf8d7f 100644 --- a/src/Utils/LegitProfiler.h +++ b/src/Utils/LegitProfiler.h @@ -121,13 +121,14 @@ namespace ImGuiUtils float GetPeakFrameTime() const { return peakFrameTime; } - void RenderTimings(int graphWidth, int legendWidth, int height, int frameIndexOffset, float maxFrameTime) + void RenderTimings(float graphWidth, float legendWidth, float height, int frameIndexOffset, float maxFrameTime, float uiScale = 1.0f) { ImDrawList* drawList = ImGui::GetWindowDrawList(); const ImVec2 widgetPos = ImGui::GetCursorScreenPos(); - RenderGraph(drawList, widgetPos, ImVec2(float(graphWidth), float(height)), frameIndexOffset, maxFrameTime); - RenderLegend(drawList, ImVec2(widgetPos.x + graphWidth, widgetPos.y), ImVec2(float(legendWidth), float(height)), frameIndexOffset, maxFrameTime); - ImGui::Dummy(ImVec2(float(graphWidth + legendWidth), float(height))); + RenderGraph(drawList, widgetPos, ImVec2(graphWidth, height), frameIndexOffset, maxFrameTime, uiScale); + if (legendWidth > 0.0f) + RenderLegend(drawList, ImVec2(widgetPos.x + graphWidth, widgetPos.y), ImVec2(legendWidth, height), frameIndexOffset, maxFrameTime, uiScale); + ImGui::Dummy(ImVec2(graphWidth + legendWidth, height)); } private: @@ -165,16 +166,18 @@ namespace ImGuiUtils } } - void RenderGraph(ImDrawList* drawList, ImVec2 graphPos, ImVec2 graphSize, size_t frameIndexOffset, float maxFrameTime) + void RenderGraph(ImDrawList* drawList, ImVec2 graphPos, ImVec2 graphSize, size_t frameIndexOffset, float maxFrameTime, float uiScale) { Rect(drawList, graphPos, ImVec2(graphPos.x + graphSize.x, graphPos.y + graphSize.y), 0xffffffff, false); - float heightThreshold = 1.0f; + const float scaledFrameWidth = std::max(1.0f, static_cast(frameWidth) * uiScale); + const float scaledFrameSpacing = std::max(1.0f, static_cast(frameSpacing) * uiScale); + const float heightThreshold = uiScale; for (size_t frameNumber = 0; frameNumber < frames.size(); frameNumber++) { size_t frameIndex = GetCurrFrameIndex(frameIndexOffset + frameNumber); - ImVec2 framePos = ImVec2(graphPos.x + graphSize.x - 1 - frameWidth - (frameWidth + frameSpacing) * float(frameNumber), graphPos.y + graphSize.y - 1); - if (framePos.x < graphPos.x + 1) + ImVec2 framePos = ImVec2(graphPos.x + graphSize.x - uiScale - scaledFrameWidth - (scaledFrameWidth + scaledFrameSpacing) * float(frameNumber), graphPos.y + graphSize.y - uiScale); + if (framePos.x < graphPos.x + uiScale) break; ImVec2 taskPos = framePos; auto& frame = frames[frameIndex]; @@ -182,22 +185,22 @@ namespace ImGuiUtils float taskStartHeight = (float(task.startTime) / maxFrameTime) * graphSize.y; float taskEndHeight = (float(task.endTime) / maxFrameTime) * graphSize.y; if (std::abs(taskEndHeight - taskStartHeight) > heightThreshold) - Rect(drawList, ImVec2(taskPos.x, taskPos.y - taskStartHeight), ImVec2(taskPos.x + frameWidth, taskPos.y - taskEndHeight), task.color, true); + Rect(drawList, ImVec2(taskPos.x, taskPos.y - taskStartHeight), ImVec2(taskPos.x + scaledFrameWidth, taskPos.y - taskEndHeight), task.color, true); } } } - void RenderLegend(ImDrawList* drawList, ImVec2 legendPos, ImVec2 legendSize, size_t frameIndexOffset, float maxFrameTime) + void RenderLegend(ImDrawList* drawList, ImVec2 legendPos, ImVec2 legendSize, size_t frameIndexOffset, float maxFrameTime, float uiScale) { - float markerLeftRectMargin = 3.0f; - float markerLeftRectWidth = 5.0f; - float markerMidWidth = 30.0f; - float markerRightRectWidth = 10.0f; - float markerRigthRectMargin = 3.0f; - float markerRightRectHeight = 10.0f; - float markerRightRectSpacing = 4.0f; - float nameOffset = 30.0f; - ImVec2 textMargin = ImVec2(5.0f, -3.0f); + float markerLeftRectMargin = 3.0f * uiScale; + float markerLeftRectWidth = 5.0f * uiScale; + float markerMidWidth = 30.0f * uiScale; + float markerRightRectWidth = 10.0f * uiScale; + float markerRigthRectMargin = 3.0f * uiScale; + float markerRightRectHeight = 10.0f * uiScale; + float markerRightRectSpacing = 4.0f * uiScale; + float nameOffset = 30.0f * uiScale; + ImVec2 textMargin = ImVec2(5.0f * uiScale, -3.0f * uiScale); auto& currFrame = frames[GetCurrFrameIndex(frameIndexOffset)]; size_t maxTasksCount = size_t(legendSize.y / (markerRightRectHeight + markerRightRectSpacing)); diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index b60471bd8e..9e6e6464ea 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1273,6 +1273,8 @@ namespace Util ImVec2 center = ImVec2(position.x + size * 0.46f, position.y + size * 0.5f); float radius = size * 0.3f; + const float circleStroke = size * ThemeManager::Constants::SEARCH_ICON_STROKE_RATIO; + const float handleStroke = size * ThemeManager::Constants::SEARCH_ICON_HANDLE_STROKE_RATIO; // Use themed text color with reduced alpha for search icon auto& theme = globals::menu->GetTheme().Palette; @@ -1281,12 +1283,12 @@ namespace Util ImU32 placeholderColor = ImGui::GetColorU32(iconColor); // Draw circle - drawList->AddCircle(center, radius, placeholderColor, 12, 2.2f); + drawList->AddCircle(center, radius, placeholderColor, 12, circleStroke); // Draw handle ImVec2 handleStart = ImVec2(center.x + radius * 0.81f, center.y + radius * 0.81f); ImVec2 handleEnd = ImVec2(handleStart.x + size * 0.29f, handleStart.y + size * 0.29f); - drawList->AddLine(handleStart, handleEnd, placeholderColor, 2.1f); + drawList->AddLine(handleStart, handleEnd, placeholderColor, handleStroke); } namespace detail @@ -1313,10 +1315,11 @@ namespace Util state.needsFocus = false; } - constexpr float iconSize = ThemeManager::Constants::COMBO_SEARCH_ICON_SIZE; + const float scale = GetSearchUIScale(); + const float iconSize = ThemeManager::Constants::COMBO_SEARCH_ICON_SIZE * scale; constexpr float iconAlpha = ThemeManager::Constants::COMBO_SEARCH_ICON_ALPHA; - constexpr float iconOffsetX = ThemeManager::Constants::COMBO_SEARCH_ICON_OFFSET_X; - constexpr float paddingLeft = ThemeManager::Constants::COMBO_SEARCH_PADDING_LEFT; + const float iconOffsetX = ThemeManager::Constants::COMBO_SEARCH_ICON_OFFSET_X * scale; + const float paddingLeft = ThemeManager::Constants::COMBO_SEARCH_PADDING_LEFT * scale; char widgetId[128]; snprintf(widgetId, sizeof(widgetId), "##%s_search", id); @@ -1346,8 +1349,9 @@ namespace Util { ImGui::PushID("FeatureSearchBar"); - float iconSize = 20.0f; - float iconSpace = iconSize + 14.0f; + const float scale = GetSearchUIScale(); + const float iconSize = ThemeManager::Constants::SEARCH_ICON_SIZE * scale; + const float iconSpace = iconSize + ThemeManager::Constants::SEARCH_INPUT_PADDING_EXTRA * scale; // Get the current cursor position and available width ImVec2 cursorPos = ImGui::GetCursorScreenPos(); @@ -1369,7 +1373,7 @@ namespace Util ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Text, textColor); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(iconSpace, ThemeManager::Constants::SEARCH_INPUT_FRAME_PADDING_Y * scale)); // Draw the input field ImGui::SetNextItemWidth(availableWidth); @@ -1382,8 +1386,8 @@ namespace Util } // Draw search icon using the reusable function - ImVec2 iconPos = ImVec2(cursorPos.x + 8.0f, cursorPos.y + (frameHeight - iconSize) * 0.5f); - DrawSearchIcon(iconPos, iconSize, 0.7f); + ImVec2 iconPos = ImVec2(cursorPos.x + ThemeManager::Constants::SEARCH_ICON_OFFSET_X * scale, cursorPos.y + (frameHeight - iconSize) * 0.5f); + DrawSearchIcon(iconPos, iconSize, ThemeManager::Constants::SEARCH_ICON_ALPHA); ImGui::PopStyleVar(2); ImGui::PopStyleColor(5); diff --git a/src/Utils/UI.h b/src/Utils/UI.h index e81bad9c23..e93babe8d8 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -10,6 +10,7 @@ #include "../FeatureConstraints.h" #include "../Menu/Fonts.h" +#include "../Menu/ThemeManager.h" #include "Utils/Input.h" // Forward declarations @@ -66,11 +67,15 @@ namespace Util // Baseline font size for UI layout scaling (1080p dynamic font: DEFAULT_SCREEN_HEIGHT * DEFAULT_FONT_RATIO). // Theme style values and pixel constants are designed for this size. - constexpr float kBaselineFontSize = 21.0f; + constexpr float kBaselineFontSize = ThemeManager::Constants::DEFAULT_SCREEN_HEIGHT * ThemeManager::Constants::DEFAULT_FONT_RATIO; - /// Returns a scale factor relative to the baseline font size, accounting for resolution and GlobalScale. - /// Use to scale hardcoded pixel sizes so layouts adapt to any font size. - inline float GetUIScale() { return ImGui::GetFontSize() / kBaselineFontSize; } + inline float GetUIScaleForBaseline(float baselineFontSize) { return ImGui::GetFontSize() / baselineFontSize; } + + /// Returns a scale factor relative to the 1080p baseline font size. + inline float GetUIScale() { return GetUIScaleForBaseline(kBaselineFontSize); } + + /// Returns a scale factor for search controls authored against the 2K baseline. + inline float GetSearchUIScale() { return GetUIScaleForBaseline(ThemeManager::Constants::SEARCH_BASELINE_SCREEN_HEIGHT * ThemeManager::Constants::DEFAULT_FONT_RATIO); } /** * Usage: @@ -865,7 +870,7 @@ namespace Util * @param size The size of the icon (default: 20.0f) * @param alpha Alpha multiplier for the icon color (default: 0.7f for subtle appearance) */ - void DrawSearchIcon(const ImVec2& position, float size = 20.0f, float alpha = 0.7f); + void DrawSearchIcon(const ImVec2& position, float size = ThemeManager::Constants::SEARCH_ICON_SIZE, float alpha = ThemeManager::Constants::SEARCH_ICON_ALPHA); /** * @brief Draws a search input field with icon inside a combo dropdown. From 84653296a2333536cdecec9e2cd81229cf8e4be4 Mon Sep 17 00:00:00 2001 From: jiayev Date: Wed, 3 Jun 2026 17:16:18 +0800 Subject: [PATCH 20/55] feat(i18n): sort translation files (#2461) --- .claude/CLAUDE.md | 10 + .github/workflows/pr-i18n.yaml | 4 + TRANSLATING.md | 19 + .../CommunityShaders/Translations/en.json | 116 +- .../CommunityShaders/Translations/zh_CN.json | 3204 ++++++++--------- src/Features/ScreenSpaceGI.cpp | 2 +- tools/sort-i18n.py | 168 + 7 files changed, 1862 insertions(+), 1661 deletions(-) create mode 100644 tools/sort-i18n.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7acc140a98..cb01ff5f49 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -615,6 +615,7 @@ The CI workflow checks: - `en.json` is in sync with source code (`--check`) - No orphaned keys exist (`--orphans`) +- Translation file key order matches `en.json` (`sort-i18n.py --check`) - Translation files have valid JSON format - Placeholders `{name}` are consistent across languages @@ -623,4 +624,13 @@ The CI workflow checks: ```bash python tools/extract-i18n.py --check python tools/extract-i18n.py --orphans +python tools/sort-i18n.py --check ``` + +If `sort-i18n.py --check` fails, fix it with: + +```bash +python tools/sort-i18n.py --write +``` + +This reorders non-English translation files so their keys follow `en.json`'s order (with `_meta` first, then keys in en.json order, then any extra keys alphabetically). diff --git a/.github/workflows/pr-i18n.yaml b/.github/workflows/pr-i18n.yaml index 0d19bec3d8..4b63de8252 100644 --- a/.github/workflows/pr-i18n.yaml +++ b/.github/workflows/pr-i18n.yaml @@ -7,6 +7,7 @@ on: - "src/**" - "package/SKSE/Plugins/CommunityShaders/Translations/**" - "tools/extract-i18n.py" + - "tools/sort-i18n.py" permissions: contents: read @@ -30,6 +31,9 @@ jobs: - name: Check for orphaned keys run: python tools/extract-i18n.py --orphans + - name: Check translation file key order matches en.json + run: python tools/sort-i18n.py --check + - name: Validate translation file formats run: | python -c " diff --git a/TRANSLATING.md b/TRANSLATING.md index 0780d4682a..83e2c2203c 100644 --- a/TRANSLATING.md +++ b/TRANSLATING.md @@ -98,9 +98,28 @@ The `pr-i18n.yaml` workflow checks: - `en.json` is in sync with source code (`--check`) - No orphaned keys exist (`--orphans`) +- Translation file key order matches `en.json` (`sort-i18n.py --check`) - Translation files have valid JSON format - Placeholders `{name}` are consistent across languages +### Translation Key Ordering + +All non-English translation files must have their keys ordered to match `en.json`. This ensures consistency and makes diffs easier to review. + +```bash +# Check if translation files are correctly ordered +python tools/sort-i18n.py --check + +# Automatically reorder translation files to match en.json +python tools/sort-i18n.py --write +``` + +Ordering rules: + +1. `_meta` always comes first +2. Keys present in `en.json` follow `en.json`'s order +3. Any extra keys not in `en.json` are appended alphabetically at the end + ### Key Naming Convention ``` diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 7d122ef376..4e6dc83057 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -997,13 +997,71 @@ "feature.renderdoc.restart_to_enable": "Requires restart to enable RenderDoc capture.", "feature.renderdoc.space_required": "At least {} MB of free space is required.", "feature.renderdoc.yes_delete": "Yes, Delete All", + "feature.screen_space_gi.ao_only": "AO only", + "feature.screen_space_gi.ao_power": "AO Power", + "feature.screen_space_gi.ao_radius": "AO radius", + "feature.screen_space_gi.ao_radius_tooltip": "A smaller radius produces tighter AO.", + "feature.screen_space_gi.blur": "Blur", + "feature.screen_space_gi.blur_radius": "Blur Radius", + "feature.screen_space_gi.buffer_viewer": "Buffer Viewer", + "feature.screen_space_gi.debug": "Debug", + "feature.screen_space_gi.denoising": "Denoising", + "feature.screen_space_gi.depth_fade_range": "Depth Fade Range", + "feature.screen_space_gi.depth_fade_range_tooltip": "Distance range where depth-based effects fade out.", "feature.screen_space_gi.description": "Screen Space Global Illumination adds realistic indirect lighting and ambient occlusion to the game. This technique simulates how light bounces off surfaces to illuminate other objects naturally.", + "feature.screen_space_gi.enabled": "Enabled", + "feature.screen_space_gi.enabled_tooltip": "Enable Screen Space Global Illumination. When disabled, all other settings are ignored.", + "feature.screen_space_gi.extreme": "Extreme", + "feature.screen_space_gi.extreme_tooltip": "Full res and clean.", + "feature.screen_space_gi.full_res": "Full Res", + "feature.screen_space_gi.geometry_weight": "Geometry Weight", + "feature.screen_space_gi.geometry_weight_tooltip": "Higher value makes the blur more sensitive to differences in geometry.", + "feature.screen_space_gi.half_res": "Half Res", + "feature.screen_space_gi.hq_specular_il": "(Experimental) HQ Specular IL", + "feature.screen_space_gi.hq_specular_il_tooltip": "An experimental specular GI that is more accurate but requires more samples. Won't be blurred.", + "feature.screen_space_gi.il_distance_compensation": "IL Distance Compensation", + "feature.screen_space_gi.il_distance_compensation_tooltip": "Brighten/Dimming further radiance samples.", + "feature.screen_space_gi.il_radius": "IL radius", + "feature.screen_space_gi.il_radius_tooltip": "A larger radius produces wider IL.", + "feature.screen_space_gi.il_saturation": "IL Saturation", + "feature.screen_space_gi.il_source_brightness": "IL Source Brightness", + "feature.screen_space_gi.indirect_lighting": "Indirect Lighting (IL)", "feature.screen_space_gi.key_feature_1": "Realistic indirect lighting", "feature.screen_space_gi.key_feature_2": "Enhanced ambient occlusion", "feature.screen_space_gi.key_feature_3": "Improved visual depth and atmosphere", "feature.screen_space_gi.key_feature_4": "Temporal denoising for smooth results", "feature.screen_space_gi.key_feature_5": "Configurable quality and performance settings", + "feature.screen_space_gi.low": "Low", + "feature.screen_space_gi.low_tooltip": "Quarter res and blurry.", + "feature.screen_space_gi.max_frame_accumulation": "Max Frame Accumulation", + "feature.screen_space_gi.max_frame_accumulation_tooltip": "How many past frames to accumulate results with. Higher values are less noisy but potentially cause ghosting.", + "feature.screen_space_gi.min_screen_radius": "Min Screen Radius", + "feature.screen_space_gi.min_screen_radius_tooltip": "The minimum screen-space effect radius as proportion of display width, to prevent far field AO being too small.", + "feature.screen_space_gi.movement_disocclusion": "Movement Disocclusion", + "feature.screen_space_gi.movement_disocclusion_tooltip": "If a pixel has moved too far from the last frame, its radiance will not be carried to this frame.\nLower values are stricter.", "feature.screen_space_gi.name": "Screen Space GI", + "feature.screen_space_gi.quality_performance": "Quality/Performance", + "feature.screen_space_gi.quarter_res": "Quarter Res", + "feature.screen_space_gi.reference": "Reference", + "feature.screen_space_gi.reference_tooltip": "Reference mode.", + "feature.screen_space_gi.shader_compile_error": "Compute shaders failed to compile!", + "feature.screen_space_gi.show_advanced": "Show Advanced Options", + "feature.screen_space_gi.slices": "Slices", + "feature.screen_space_gi.slices_tooltip": "How many directions do the samples take.\nControls noise.", + "feature.screen_space_gi.standard": "Standard", + "feature.screen_space_gi.standard_tooltip": "Half res and somewhat stable.", + "feature.screen_space_gi.steps_per_slice": "Steps Per Slice", + "feature.screen_space_gi.steps_per_slice_tooltip": "How many samples does it take in one direction.\nControls accuracy of lighting, and noise when effect radius is large.", + "feature.screen_space_gi.temporal_denoiser": "Temporal Denoiser", + "feature.screen_space_gi.thickness": "Thickness", + "feature.screen_space_gi.thickness_tooltip": "How thick the occluders are. Only affects AO.", + "feature.screen_space_gi.toggles": "Toggles", + "feature.screen_space_gi.vanilla_ssao": "Vanilla SSAO", + "feature.screen_space_gi.vanilla_ssao_tooltip": "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.", + "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "Vanilla SSAO is not supported in VR.", + "feature.screen_space_gi.view_resize": "View Resize", + "feature.screen_space_gi.visual": "Visual", + "feature.screen_space_gi.visual_il": "Visual - IL", "feature.screen_space_gi.vr_warning": "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects.", "feature.screen_space_shadows.bilinear_threshold": "Bilinear Threshold", "feature.screen_space_shadows.bilinear_threshold_tooltip": "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.", @@ -1154,64 +1212,6 @@ "feature.skylighting.rebuild": "Rebuild Skylighting", "feature.skylighting.rebuild_tooltip": "Changes below require rebuilding, a loading screen, or moving away from the current location to apply.", "feature.skylighting.specular_min_visibility": "Specular Min Visibility", - "feature.ssgi.ao_only": "AO only", - "feature.ssgi.ao_power": "AO Power", - "feature.ssgi.ao_radius": "AO radius", - "feature.ssgi.ao_radius_tooltip": "A smaller radius produces tighter AO.", - "feature.ssgi.blur": "Blur", - "feature.ssgi.blur_radius": "Blur Radius", - "feature.ssgi.buffer_viewer": "Buffer Viewer", - "feature.ssgi.debug": "Debug", - "feature.ssgi.denoising": "Denoising", - "feature.ssgi.depth_fade_range": "Depth Fade Range", - "feature.ssgi.depth_fade_range_tooltip": "Distance range where depth-based effects fade out.", - "feature.ssgi.enabled": "Enabled", - "feature.ssgi.enabled_tooltip": "Enable Screen Space Global Illumination. When disabled, all other settings are ignored.", - "feature.ssgi.extreme": "Extreme", - "feature.ssgi.extreme_tooltip": "Full res and clean.", - "feature.ssgi.full_res": "Full Res", - "feature.ssgi.geometry_weight": "Geometry Weight", - "feature.ssgi.geometry_weight_tooltip": "Higher value makes the blur more sensitive to differences in geometry.", - "feature.ssgi.half_res": "Half Res", - "feature.ssgi.hq_specular_il": "(Experimental) HQ Specular IL", - "feature.ssgi.hq_specular_il_tooltip": "An experimental specular GI that is more accurate but requires more samples. Won't be blurred.", - "feature.ssgi.il_distance_compensation": "IL Distance Compensation", - "feature.ssgi.il_distance_compensation_tooltip": "Brighten/Dimming further radiance samples.", - "feature.ssgi.il_radius": "IL radius", - "feature.ssgi.il_radius_tooltip": "A larger radius produces wider IL.", - "feature.ssgi.il_saturation": "IL Saturation", - "feature.ssgi.il_source_brightness": "IL Source Brightness", - "feature.ssgi.indirect_lighting": "Indirect Lighting (IL)", - "feature.ssgi.low": "Low", - "feature.ssgi.low_tooltip": "Quarter res and blurry.", - "feature.ssgi.max_frame_accumulation": "Max Frame Accumulation", - "feature.ssgi.max_frame_accumulation_tooltip": "How many past frames to accumulate results with. Higher values are less noisy but potentially cause ghosting.", - "feature.ssgi.min_screen_radius": "Min Screen Radius", - "feature.ssgi.min_screen_radius_tooltip": "The minimum screen-space effect radius as proportion of display width, to prevent far field AO being too small.", - "feature.ssgi.movement_disocclusion": "Movement Disocclusion", - "feature.ssgi.movement_disocclusion_tooltip": "If a pixel has moved too far from the last frame, its radiance will not be carried to this frame.\nLower values are stricter.", - "feature.ssgi.quality_performance": "Quality/Performance", - "feature.ssgi.quarter_res": "Quarter Res", - "feature.ssgi.reference": "Reference", - "feature.ssgi.reference_tooltip": "Reference mode.", - "feature.ssgi.shader_compile_error": "Compute shaders failed to compile!", - "feature.ssgi.show_advanced": "Show Advanced Options", - "feature.ssgi.slices": "Slices", - "feature.ssgi.slices_tooltip": "How many directions do the samples take.\nControls noise.", - "feature.ssgi.standard": "Standard", - "feature.ssgi.standard_tooltip": "Half res and somewhat stable.", - "feature.ssgi.steps_per_slice": "Steps Per Slice", - "feature.ssgi.steps_per_slice_tooltip": "How many samples does it take in one direction.\nControls accuracy of lighting, and noise when effect radius is large.", - "feature.ssgi.temporal_denoiser": "Temporal Denoiser", - "feature.ssgi.thickness": "Thickness", - "feature.ssgi.thickness_tooltip": "How thick the occluders are. Only affects AO.", - "feature.ssgi.toggles": "Toggles", - "feature.ssgi.vanilla_ssao": "Vanilla SSAO", - "feature.ssgi.vanilla_ssao_tooltip": "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.", - "feature.ssgi.vanilla_ssao_tooltip_vr": "Vanilla SSAO is not supported in VR.", - "feature.ssgi.view_resize": "View Resize", - "feature.ssgi.visual": "Visual", - "feature.ssgi.visual_il": "Visual - IL", "feature.sss.base_profile": "Base Profile", "feature.sss.blur_radius": "Blur Radius", "feature.sss.blur_radius_tooltip": "Blur radius.", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index f5c36c0f7f..6382ae69af 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -3,1608 +3,10 @@ "language": "简体中文", "locale": "zh_CN", "version": "1.0.0", - "authors": ["Community Shaders Team"] + "authors": "Jiaye" }, "common.active": "激活", "common.inactive": "未激活", - "feature.category.characters": "角色", - "feature.category.display": "显示", - "feature.category.grass": "草地", - "feature.category.landscape_and_textures": "地形与纹理", - "feature.category.lighting": "光照", - "feature.category.materials": "材质", - "feature.category.other": "其他", - "feature.category.post_processing": "后处理", - "feature.category.sky": "天空", - "feature.category.utility": "工具", - "feature.category.water": "水面", - "feature.cloud_shadows.description": "为地形和物体投射逼真的云阴影,当云层掠过时产生动态的光影变化,增强氛围沉浸感。", - "feature.cloud_shadows.key_feature_1": "地形与物体的动态云阴影投射", - "feature.cloud_shadows.key_feature_2": "可配置的阴影不透明度,便于艺术控制", - "feature.cloud_shadows.key_feature_3": "与云层运动同步的实时阴影移动", - "feature.cloud_shadows.key_feature_4": "基于立方体贴图的精确阴影投射计算", - "feature.cloud_shadows.key_feature_5": "增强的天空渲染集成", - "feature.cloud_shadows.name": "云阴影", - "feature.cloud_shadows.opacity": "不透明度", - "feature.cloud_shadows.opacity_tooltip": "值越高,云阴影越暗。", - "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", - "feature.dynamic_cubemaps.color": "颜色", - "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", - "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", - "feature.dynamic_cubemaps.dynamic_cubemap_creator": "动态立方体贴图创建器", - "feature.dynamic_cubemaps.enable_creator": "启用创建器", - "feature.dynamic_cubemaps.enable_ssr": "启用屏幕空间反射", - "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", - "feature.dynamic_cubemaps.export": "导出", - "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", - "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", - "feature.dynamic_cubemaps.key_feature_3": "增强的水面反射,包含环境细节", - "feature.dynamic_cubemaps.key_feature_4": "支持标准和VR两种渲染模式", - "feature.dynamic_cubemaps.key_feature_5": "优化的立方体贴图推理与辐照度计算", - "feature.dynamic_cubemaps.name": "动态立方体贴图", - "feature.dynamic_cubemaps.roughness": "粗糙度", - "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", - "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", - "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", - "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", - "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", - "feature.exp_height_fog.debug": "调试", - "feature.exp_height_fog.depth_distribution_scale": "深度分布比例", - "feature.exp_height_fog.dir_inscattering_anisotropy": "方向光内散射各向异性", - "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "通过Henyey-Greenstein相位函数控制内散射的不对称性。\n正值产生前向散射(太阳周围发光)。\n零为各向同性。负值产生后向散射。", - "feature.exp_height_fog.dir_inscattering_mul": "方向光内散射倍率", - "feature.exp_height_fog.directional_scattering_intensity": "方向光散射强度", - "feature.exp_height_fog.directional_shadow_bias": "方向阴影偏移", - "feature.exp_height_fog.disable_vanilla_fog": "禁用原版雾", - "feature.exp_height_fog.disable_vanilla_fog_tooltip": "完全禁用原版雾。仅应用指数高度雾。", - "feature.exp_height_fog.enable_exp_height_fog": "启用指数高度雾", - "feature.exp_height_fog.enable_volumetric_fog": "启用体积雾", - "feature.exp_height_fog.fog_density": "雾密度", - "feature.exp_height_fog.fog_height": "雾高度", - "feature.exp_height_fog.fog_height_falloff": "雾高度衰减", - "feature.exp_height_fog.fog_inscattering_color": "雾内散射颜色", - "feature.exp_height_fog.grid_depth_slices": "网格深度切片", - "feature.exp_height_fog.grid_pixel_size": "网格像素大小", - "feature.exp_height_fog.history_miss_samples": "历史缺失采样数", - "feature.exp_height_fog.inscattering_cubemap_tint": "内散射立方体贴图色调", - "feature.exp_height_fog.local_light_scattering_intensity": "局部光散射强度", - "feature.exp_height_fog.near_fade_in_distance": "近处淡入距离", - "feature.exp_height_fog.original_fog_color_amount": "原始雾颜色量", - "feature.exp_height_fog.sample_jitter_multiplier": "采样抖动倍率", - "feature.exp_height_fog.sample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.LightScatteringSampleJitterMultiplier。\n在 Halton 序列基础上为每个体素添加随机偏移。\n0 = UE 默认值;非零值需要更强的时域滤波。", - "feature.exp_height_fog.sky_lighting_scattering_intensity": "天空光照散射强度", - "feature.exp_height_fog.start_distance": "起始距离", - "feature.exp_height_fog.sunlight_attenuation": "阳光衰减量", - "feature.exp_height_fog.temporal_history_weight": "时域历史权重", - "feature.exp_height_fog.upsample_jitter_multiplier": "上采样抖动倍率", - "feature.exp_height_fog.upsample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.UpsampleJitterMultiplier。\n在屏幕空间抖动最终 3D 雾查找,以隐藏\n低分辨率 froxel 像素化。0 = UE 默认值。", - "feature.exp_height_fog.use_dynamic_cubemaps": "使用动态立方体贴图进行内散射", - "feature.exp_height_fog.volumetric_albedo": "体积雾反照率", - "feature.exp_height_fog.volumetric_emissive": "体积雾自发光", - "feature.exp_height_fog.volumetric_extinction_scale": "体积雾消光比例", - "feature.exp_height_fog.volumetric_fog": "体积雾", - "feature.exp_height_fog.volumetric_scattering_distribution": "体积雾散射分布", - "feature.exp_height_fog.volumetric_start_distance": "体积雾起始距离", - "feature.exp_height_fog.volumetric_view_distance": "体积雾视距", - "feature.exponential_height_fog.description": "添加逼真的高度雾效果,雾密度随高度变化,增强场景的大气深度和沉浸感。", - "feature.exponential_height_fog.key_feature_1": "新增指数高度雾效果", - "feature.exponential_height_fog.key_feature_2": "适配原版雾效设置", - "feature.exponential_height_fog.key_feature_3": "营造大气深度感", - "feature.exponential_height_fog.name": "指数高度雾", - "feature.extended_materials.complex_material": "复杂材质", - "feature.extended_materials.description": "扩展材质添加了包括视差遮蔽映射和复杂材质混合在内的高级材质效果。\n此功能可增强表面细节和深度感知,呈现更逼真的纹理。", - "feature.extended_materials.enable_complex_material": "启用复杂材质", - "feature.extended_materials.enable_complex_material_tooltip": "启用利用环境遮罩的复杂材质规范支持。包括视差贴图,以及更逼真的金属和镜面反射。对于环境遮罩中alpha通道无效的模组内容,可能导致纹理变形。", - "feature.extended_materials.enable_height_blending": "启用地形高度混合", - "feature.extended_materials.enable_height_blending_tooltip": "基于视差启用地形纹理混合。", - "feature.extended_materials.enable_legacy_terrain": "启用旧版地形", - "feature.extended_materials.enable_legacy_terrain_tooltip": "使用每张地形纹理的alpha通道启用地形视差。因此,所有地形纹理必须支持视差才能使效果正常工作。", - "feature.extended_materials.enable_parallax": "启用视差", - "feature.extended_materials.enable_parallax_tooltip": "在为视差制作的标准网格上启用视差效果。", - "feature.extended_materials.enable_parallax_warping_fix": "启用视差变形修复", - "feature.extended_materials.enable_parallax_warping_fix_tooltip": "启用修复,减少弯曲和平滑法线三角形上的视差缩放。", - "feature.extended_materials.enable_shadows": "启用阴影", - "feature.extended_materials.enable_shadows_tooltip": "使用视差时启用廉价软阴影。适用于所有方向光和点光源。", - "feature.extended_materials.extend_shadows": "扩展阴影", - "feature.extended_materials.extend_shadows_tooltip": "将视差阴影扩展到视差范围之外。对性能影响较小。", - "feature.extended_materials.key_feature_1": "视差遮蔽映射,增加深度感", - "feature.extended_materials.key_feature_2": "复杂材质混合", - "feature.extended_materials.key_feature_3": "地形高度图支持", - "feature.extended_materials.key_feature_4": "视差阴影", - "feature.extended_materials.key_feature_5": "基于高度的纹理混合", - "feature.extended_materials.name": "扩展材质", - "feature.extended_materials.parallax": "视差", - "feature.extended_materials.soft_shadows": "近似软阴影", - "feature.extended_translucency.alpha_mode_anisotropic_fabric": "3 - 各向异性织物", - "feature.extended_translucency.alpha_mode_disabled": "0 - 禁用", - "feature.extended_translucency.alpha_mode_isotropic_fabric": "2 - 各向同性织物、玻璃等", - "feature.extended_translucency.alpha_mode_rim_edge": "1 - 边缘光", - "feature.extended_translucency.blend_weight": "混合权重", - "feature.extended_translucency.blend_weight_tooltip": "控制效果应用于最终结果的混合权重。", - "feature.extended_translucency.default_material_model": "默认材质模型", - "feature.extended_translucency.default_material_model_tooltip": "各向异性半透明将根据您查看半透明表面的视角调整不透明度。\n - 禁用:无各向异性半透明,平坦Alpha。\n - 边缘光:无物理模型的简单边缘光效果,几何体边缘始终不透明,即使完全透明。\n - 各向同性织物:由单一方向编织的虚构织物,尊重法线贴图,也适用于玻璃面板层。\n - 各向异性织物:由切线和副法线方向编织的常见织物,忽略法线贴图。\n", - "feature.extended_translucency.description": "为薄织物和其他半透明材质提供逼真的渲染效果。\n支持多种材质模型,适用于不同类型的半透明表面。", - "feature.extended_translucency.key_feature_1": "多种半透明材质模型(边缘光、各向同性/各向异性织物)", - "feature.extended_translucency.key_feature_2": "逼真的织物半透明效果,支持方向光透射", - "feature.extended_translucency.key_feature_3": "通过NIF额外数据支持逐材质覆写", - "feature.extended_translucency.key_feature_4": "可配置的透明度与柔和度控制", - "feature.extended_translucency.key_feature_5": "性能优化的半透明计算", - "feature.extended_translucency.name": "扩展半透明", - "feature.extended_translucency.skinned_mesh_only": "仅蒙皮网格", - "feature.extended_translucency.skinned_mesh_only_tooltip": "控制此效果是否仅应用于蒙皮网格。如果在随机对象上看到不期望的效果,请勾选此选项。", - "feature.extended_translucency.softness": "柔和度", - "feature.extended_translucency.softness_tooltip": "控制Alpha增加的柔和度,增加柔和度会减少Alpha的增加量。", - "feature.extended_translucency.translucent_material": "半透明材质", - "feature.extended_translucency.transparency_increase": "透明度增加", - "feature.extended_translucency.transparency_increase_tooltip": "半透明材质会使材质平均更不透明,这可能与预期不同。降低Alpha以抵消此效果并增加输出的动态范围。", - "feature.grass_collision.description": "启用动态草地交互——当角色走过草地时,草会弯曲和摆动,营造更沉浸的环境反应。", - "feature.grass_collision.enable": "启用草地碰撞", - "feature.grass_collision.grass_collision": "草地碰撞", - "feature.grass_collision.key_feature_1": "角色移动带动实时草地变形", - "feature.grass_collision.key_feature_2": "最多支持256个同时交互的碰撞检测", - "feature.grass_collision.key_feature_3": "动态追踪角色位置以驱动草地响应", - "feature.grass_collision.key_feature_4": "性能优化的碰撞计算", - "feature.grass_collision.key_feature_5": "与现有草地渲染无缝集成", - "feature.grass_collision.name": "草地碰撞", - "feature.grass_lighting.basic_grass": "基础草地", - "feature.grass_lighting.brightness": "亮度", - "feature.grass_lighting.brightness_tooltip": "将草地纹理变暗,以便在新光照下看起来更好", - "feature.grass_lighting.complex_grass": "复杂草地", - "feature.grass_lighting.description": "通过改进的光照、高光和次表面散射,增强草地渲染效果。\n使草地看起来更自然,对光照条件反应更灵敏。", - "feature.grass_lighting.detection_header": "复杂草地检测", - "feature.grass_lighting.detection_threshold": "检测阈值", - "feature.grass_lighting.detection_threshold_tooltip": "检测复杂草地纹理的阈值。值越低越严格。", - "feature.grass_lighting.effects": "效果", - "feature.grass_lighting.glossiness": "光泽度", - "feature.grass_lighting.glossiness_tooltip": "高光光泽度。", - "feature.grass_lighting.key_feature_1": "增强的草地光照模型", - "feature.grass_lighting.key_feature_2": "草地上的高光反射", - "feature.grass_lighting.key_feature_3": "次表面散射效果", - "feature.grass_lighting.key_feature_4": "提升草地视觉质量", - "feature.grass_lighting.key_feature_5": "可配置的材质属性", - "feature.grass_lighting.lighting": "光照", - "feature.grass_lighting.name": "草地光照", - "feature.grass_lighting.override_complex": "覆盖复杂草地光照设置", - "feature.grass_lighting.override_complex_tooltip": "覆盖草地网格作者设置的参数。复杂草地作者可以为其草地网格定义亮度。然而,某些作者可能未考虑Community Shaders提供的额外光源。此选项将其草地设置视为非复杂草地。这是Community Shaders < 0.7.0中的默认行为", - "feature.grass_lighting.specular_desc": "复杂草地的高光", - "feature.grass_lighting.specular_strength": "高光强度", - "feature.grass_lighting.specular_strength_tooltip": "高光强度。", - "feature.grass_lighting.sss_amount": "SSS量", - "feature.grass_lighting.sss_tooltip": "次表面散射(SSS)量。柔和光照控制物体的均匀照明程度。背光照明照亮物体的背面。两者结合模拟光线穿过表面的传输。", - "feature.hair_specular.description": "提供更好的头发着色效果,具有逼真的高光反射和基于切线的光线交互,呈现更生动的头发外观。", - "feature.hair_specular.diffuse_multiplier": "漫反射倍率", - "feature.hair_specular.enable_self_shadow": "启用屏幕空间自阴影", - "feature.hair_specular.enable_self_shadow_tooltip": "为头发启用屏幕空间自阴影。\nMarschner头发模型在没有自阴影的情况下可能会有过亮的透射。\n", - "feature.hair_specular.enable_tangent_shift": "启用切线偏移", - "feature.hair_specular.enable_tangent_shift_tooltip": "启用使用切线偏移纹理来改变发丝上的高光变化。\n结果可能因使用的头发模型而异。\n", - "feature.hair_specular.enabled": "启用", - "feature.hair_specular.glossiness": "光泽度", - "feature.hair_specular.glossiness_tooltip": "控制头发的光泽度。\nKajiya-Kay模式中光泽度映射到高光指数。\nMarschner模式中控制头发表面的粗糙度。\n", - "feature.hair_specular.hair_base_color_multiplier": "头发基色倍率", - "feature.hair_specular.hair_mode": "头发模式", - "feature.hair_specular.hair_mode_tooltip": "选择要使用的头发着色模型。\nKajiya-Kay是模拟头发高光的经验模型。\nMarschner是更基于物理的模型,模拟头发光交互。\n两种模型都是各向异性的,支持基于切线的着色。\n没有自阴影时,Marschner可能因透射而显得过亮。\n", - "feature.hair_specular.hair_saturation": "头发饱和度", - "feature.hair_specular.indirect_diffuse_multiplier": "间接漫反射倍率", - "feature.hair_specular.indirect_specular_multiplier": "间接高光倍率", - "feature.hair_specular.key_feature_1": "逼真的头发高光反射", - "feature.hair_specular.key_feature_2": "增强的头发光泽度与饱和度控制", - "feature.hair_specular.key_feature_3": "独立的高光和漫反射光照倍率", - "feature.hair_specular.key_feature_4": "切线偏移纹理支持,实现多样的头发高光效果", - "feature.hair_specular.name": "头发高光", - "feature.hair_specular.primary_tangent_shift": "主高光切线偏移", - "feature.hair_specular.secondary_tangent_shift": "次高光切线偏移", - "feature.hair_specular.self_shadow_exponent": "自阴影指数", - "feature.hair_specular.self_shadow_scale": "自阴影缩放", - "feature.hair_specular.self_shadow_strength": "自阴影强度", - "feature.hair_specular.specular_multiplier": "高光倍率", - "feature.hair_specular.transmission": "透射", - "feature.hdr_display.advanced": "高级", - "feature.hdr_display.advanced_tooltip_enable_windows_hdr": "建议启用 Windows HDR,而不是在这里强制开启。", - "feature.hdr_display.advanced_tooltip_force_enable": "即使未检测到,也强制启用 HDR(不推荐)。", - "feature.hdr_display.cancel": "取消", - "feature.hdr_display.capable_display_windows_hdr_off": "支持 HDR 的显示器(Windows HDR 已关闭)", - "feature.hdr_display.capable_display_windows_hdr_off_tooltip_0": "你的显示器支持 HDR,但 Windows HDR 当前已关闭。", - "feature.hdr_display.capable_display_windows_hdr_off_tooltip_1": "请在 Windows 显示设置中启用 HDR,以允许自动检测。", - "feature.hdr_display.description": "为HDR显示器提供真正的高动态范围输出。", - "feature.hdr_display.display_detected": "检测到 HDR 显示器", - "feature.hdr_display.display_reports_max_nits": "显示器报告的最大亮度:%.0f 尼特", - "feature.hdr_display.display_reports_max_nits_tooltip_0": "该值由操作系统或驱动(DXGI MaxLuminance)报告,并非直接测量值。", - "feature.hdr_display.display_reports_max_nits_tooltip_1": "它可能来自 EDID 元数据,因此可能与真实高光峰值亮度不同。", - "feature.hdr_display.display_reports_max_nits_tooltip_2": "请把它当作初始参考值,并按需调节峰值亮度。", - "feature.hdr_display.dont_show_again": "不再显示此提示", - "feature.hdr_display.enable_hdr": "启用 HDR", - "feature.hdr_display.enable_hdr_tooltip": "启用 HDR 输出。在扩展动态范围下尽量保持与原版相近的视觉效果。", - "feature.hdr_display.enable_hdr_tooltip_not_detected": "未检测到 HDR 显示器。可使用“高级”按钮强制开启。", - "feature.hdr_display.enable_hdr_tooltip_windows_off": "显示器支持 HDR,但 Windows HDR 已关闭。请先在 Windows 显示设置中启用 HDR,然后重启游戏。", - "feature.hdr_display.enabled_without_detected_display": "HDR 已启用,但未检测到 HDR 显示器。", - "feature.hdr_display.exclusive_fullscreen_warning": "警告:检测到独占全屏模式。", - "feature.hdr_display.exclusive_fullscreen_warning_detail": "HDR 与独占全屏不兼容,可能无法正常工作。请切换到无边框窗口模式以获得正确的 HDR 支持。", - "feature.hdr_display.force_enable_hdr": "强制启用 HDR", - "feature.hdr_display.force_enable_hdr_confirm": "仅当你确实拥有 HDR 显示器,但它未被正确检测到时,才应继续。", - "feature.hdr_display.force_enable_hdr_detected_warning": "未在你的显示器上检测到 HDR。", - "feature.hdr_display.force_enable_hdr_sdr_warning": "如果你使用的是 SDR(标准动态范围)显示器,游戏画面会非常不正常。", - "feature.hdr_display.force_enable_hdr_warning": "警告:强制启用 HDR", - "feature.hdr_display.key_feature_1": "支持HDR10输出(10位色深),升级HDR缓冲区至16位,完全无裁剪的渲染管线以实现真正的HDR数值。", - "feature.hdr_display.key_feature_2": "基于Skyrim ISHDR路径的HDR感知色调映射(Reinhard/Hejl-Burgess-Dawson),在保留原版风格的同时改善HDR显示器上的高光处理。", - "feature.hdr_display.key_feature_3": "可配置的纸张白点和峰值亮度。", - "feature.hdr_display.name": "HDR 显示", - "feature.hdr_display.paper_white_nits": "纸白亮度(尼特)", - "feature.hdr_display.paper_white_tooltip_0": "控制 SDR 白色在 HDR 显示器上的显示亮度。", - "feature.hdr_display.paper_white_tooltip_1": "203 尼特是 ITU BT.2408 参考值。提高该值可获得更亮的画面。", - "feature.hdr_display.peak_brightness_nits": "峰值亮度(尼特)", - "feature.hdr_display.peak_brightness_tooltip_0": "显示器可输出的最大亮度。", - "feature.hdr_display.peak_brightness_tooltip_1": "请设置为与你显示器真实峰值亮度相匹配的数值。", - "feature.hdr_display.sdr_display_not_detected": "SDR 显示器(未检测到 HDR)", - "feature.hdr_display.ui_brightness_multiplier": "UI 亮度倍率", - "feature.hdr_display.ui_brightness_multiplier_tooltip_0": "在 HDR 模式下,UI 亮度 = 纸白亮度 x 此倍率。", - "feature.hdr_display.ui_brightness_multiplier_tooltip_1": "1.00x 表示 UI 以纸白亮度渲染。更高的值会让 UI 相对场景内容更亮。", - "feature.hdr_display.ui_brightness_multiplier_tooltip_2": "注意:主菜单和加载画面始终以纸白亮度渲染。", - "feature.hdr_display.warning_popup_title": "HDR 警告", - "feature.ibl.dalc_amount": "DALC量", - "feature.ibl.dalc_amount_tooltip": "将IBL亮度向游戏原版环境光(DALC)级别混合。\n0 = 不匹配(纯IBL亮度),1 = 完全匹配原版环境光。", - "feature.ibl.dalc_mode": "DALC模式", - "feature.ibl.dalc_mode_color_ratio": "颜色比例", - "feature.ibl.dalc_mode_dalc_plus_sky": "DALC + 天空", - "feature.ibl.dalc_mode_dalc_plus_sky_directional": "DALC + 天空(定向)", - "feature.ibl.dalc_mode_luminance_ratio": "亮度比例", - "feature.ibl.dalc_mode_tooltip": "DALC与IBL亮度比率的计算方式:\n亮度比:来自总亮度的标量比率(丢失DALC颜色色调)。\n颜色比:逐通道比率(保留DALC颜色色调)。\nDALC + 天空:使用原版环境光作为基础,天空IBL叠加。天光仅影响天空。\nDALC + 天空(方向性):相同,但天光也按方向降低原版环境光。", - "feature.ibl.description": "用基于物理的IBL替代游戏的环境光照,IBL从立方体贴图的球谐函数中推导得出。", - "feature.ibl.disable_in_interiors": "在室内禁用", - "feature.ibl.disable_in_interiors_tooltip": "在室内单元中禁用IBL。", - "feature.ibl.enable_ibl": "启用IBL", - "feature.ibl.enable_ibl_tooltip": "切换IBL。启用时,环境光来自立方体贴图球谐函数,而非原版系统。", - "feature.ibl.env_ibl_saturation": "环境IBL饱和度", - "feature.ibl.env_ibl_saturation_tooltip": "环境IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", - "feature.ibl.env_ibl_scale": "环境IBL缩放", - "feature.ibl.env_ibl_scale_tooltip": "环境IBL的强度倍率(来自动态立方体贴图)。\n控制周围环境对环境光照的贡献强度。", - "feature.ibl.fog_mix": "雾混合", - "feature.ibl.fog_mix_tooltip": "将雾颜色向IBL环境光颜色混合。\n0 = 原版雾,1 = 雾完全由IBL着色。", - "feature.ibl.key_feature_1": "将环境和天空立方体贴图投影为球谐函数(SH)以计算辐照度", - "feature.ibl.key_feature_2": "双IBL源:环境立方体贴图(动态立方体贴图)和Skyrim原生天空反射立方体贴图", - "feature.ibl.key_feature_3": "DALC亮度匹配,保持IBL与游戏环境光水平一致", - "feature.ibl.key_feature_4": "可配置的每源强度、饱和度、雾混合以及每天气覆写", - "feature.ibl.key_feature_5": "静态IBL回退纹理,用于世界外对象(如物品栏物品)", - "feature.ibl.name": "基于图像的光照", - "feature.ibl.preserve_fog_luminance": "保持雾亮度", - "feature.ibl.preserve_fog_luminance_tooltip": "当雾混合激活时,重新缩放IBL着色的雾以保持原始雾亮度。\n防止雾变得过亮或过暗。", - "feature.ibl.sky_ibl_saturation": "天空IBL饱和度", - "feature.ibl.sky_ibl_saturation_tooltip": "天空IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", - "feature.ibl.sky_ibl_scale": "天空IBL缩放", - "feature.ibl.sky_ibl_scale_tooltip": "天空IBL的强度倍率(来自游戏的原始反射立方体贴图)。\n控制天空对环境光照的贡献强度。", - "feature.ibl.use_static_ibl": "对世界外物体使用静态IBL", - "feature.ibl.use_static_ibl_tooltip": "对在游戏世界外渲染的物体(如物品栏物品、加载画面)使用预烘焙的静态IBL立方体贴图纹理。", - "feature.interior_sun.description": "允许太阳和月亮的光线和阴影照射到室内空间。", - "feature.interior_sun.force_double_sided": "强制双面渲染", - "feature.interior_sun.force_double_sided_tooltip": "在室内太阳阴影贴图渲染期间禁用背面剔除。将防止大部分通过未遮罩/未准备好的室内的漏光,性能成本较小。", - "feature.interior_sun.interior_shadow_distance": "室内阴影距离", - "feature.interior_sun.interior_shadow_distance_tooltip": "设置在室内渲染阴影的距离。较低值提供更高质量的阴影并改善性能,但可能导致远处室内空间照亮不正确。", - "feature.interior_sun.key_feature_1": "仅对明确启用的室内空间生效", - "feature.interior_sun.key_feature_2": "利用现有的太阳、月亮和天气系统", - "feature.interior_sun.key_feature_3": "包含强制双面渲染选项,适用于未准备的室内场景", - "feature.interior_sun.key_feature_4": "修复导致漏光的几何体裁剪问题", - "feature.interior_sun.name": "室内阳光", - "feature.inverse_square_lighting.description": "为光照实现额外的平方反比衰减,使光照衰减更加物理准确和逼真。", - "feature.inverse_square_lighting.key_feature_1": "基于强度自动计算光照半径", - "feature.inverse_square_lighting.key_feature_2": "光源在可配置的截止距离处平滑淡出,解决无限距离问题", - "feature.inverse_square_lighting.key_feature_3": "不修改任何现有光照", - "feature.inverse_square_lighting.key_feature_4": "需要使用开启了平方反比衰减的模组光源。", - "feature.inverse_square_lighting.key_feature_5": "与Light Placer完全集成", - "feature.inverse_square_lighting.name": "平方反比光照", - "feature.key_features": "主要特性:", - "feature.light_editor.active_shadow_lights": "活跃阴影光源:%u", - "feature.light_editor.base_object": "基础对象:0x%08X | %s", - "feature.light_editor.cell": "单元格:%s", - "feature.light_editor.color": "颜色", - "feature.light_editor.cutoff": "截止", - "feature.light_editor.disable_inverse_square_falloff_lights": "禁用平方反比衰减光源", - "feature.light_editor.disable_regular_falloff_lights": "禁用常规衰减光源", - "feature.light_editor.dynamic": "动态", - "feature.light_editor.filter_by": "过滤方式", - "feature.light_editor.flicker": "闪烁", - "feature.light_editor.flicker_slow": "缓慢闪烁", - "feature.light_editor.hemi_shadow": "半球阴影", - "feature.light_editor.intensity": "强度", - "feature.light_editor.inverse_square_light": "平方反比光源", - "feature.light_editor.ligh": "LIGH:0x%08X | %s", - "feature.light_editor.light_flags": "光源标志", - "feature.light_editor.lights": "光源", - "feature.light_editor.linear_light": "线性光源", - "feature.light_editor.memory_address": "内存地址:%p", - "feature.light_editor.negative": "负向", - "feature.light_editor.ni_light_name": "NiLight名称:%s", - "feature.light_editor.omni_shadow": "全向阴影", - "feature.light_editor.owner": "所有者:0x%08X | %s", - "feature.light_editor.owner_last_edited_by": "所有者最后编辑者:%s", - "feature.light_editor.portal_strict": "传送门严格", - "feature.light_editor.position_format": "X:%.2f,Y:%.2f,Z:%.2f", - "feature.light_editor.position_offset": "位置偏移", - "feature.light_editor.pulse": "脉冲", - "feature.light_editor.pulse_slow": "缓慢脉冲", - "feature.light_editor.radius": "半径", - "feature.light_editor.revert_changes": "还原更改", - "feature.light_editor.save_to_light_placer": "保存到Light Placer", - "feature.light_editor.save_to_light_placer_tooltip": "将当前设置保存到Light Placer JSON。", - "feature.light_editor.select_a_light": "选择光源", - "feature.light_editor.shadows_only": "仅阴影", - "feature.light_editor.shadows_only_tooltip": "仅显示带有HemiShadow或OmniShadow标志的光源。", - "feature.light_editor.size": "大小", - "feature.light_editor.sort_by": "排序方式", - "feature.light_editor.spotlight_not_applicable": "聚光灯:ISL光源类型标志不适用", - "feature.light_editor.total_lights": "总光源数:%u", - "feature.light_limit_fix.debug": "调试", - "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", - "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", - "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", - "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", - "feature.light_limit_fix.key_feature_1": "移除4光源限制", - "feature.light_limit_fix.key_feature_2": "无限动态光源", - "feature.light_limit_fix.key_feature_3": "提升光照质量", - "feature.light_limit_fix.key_feature_4": "增强视觉真实感", - "feature.light_limit_fix.key_feature_5": "增强视觉真实感", - "feature.light_limit_fix.light_limit_vis": "光源限制可视化", - "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", - "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", - "feature.light_limit_fix.name": "光源限制修复", - "feature.light_limit_fix.statistics": "统计", - "feature.linear_lighting.ambient_gamma": "环境伽马", - "feature.linear_lighting.ambient_multiplier": "环境倍率", - "feature.linear_lighting.blood_effects_multiplier": "血液效果倍率", - "feature.linear_lighting.color_gamma": "颜色伽马", - "feature.linear_lighting.deferred_effects_multiplier": "延迟效果倍率", - "feature.linear_lighting.description": "通过色彩空间转换来提高光照计算的准确性。", - "feature.linear_lighting.directional_light_multiplier": "方向光倍率", - "feature.linear_lighting.effect_gamma": "效果伽马", - "feature.linear_lighting.effect_lighting_multiplier": "效果光照倍率", - "feature.linear_lighting.effect_transparency_gamma": "效果透明度伽马", - "feature.linear_lighting.effects": "效果", - "feature.linear_lighting.emissive_color_gamma": "自发光颜色伽马", - "feature.linear_lighting.emissive_color_multiplier": "自发光颜色倍率", - "feature.linear_lighting.enable": "启用线性光照", - "feature.linear_lighting.fog_gamma": "雾伽马", - "feature.linear_lighting.fog_transparency_gamma": "雾透明度伽马", - "feature.linear_lighting.gamma_settings": "伽马设置", - "feature.linear_lighting.glowmap_gamma": "发光贴图伽马", - "feature.linear_lighting.glowmap_multiplier": "发光贴图倍率", - "feature.linear_lighting.key_feature_1": "可自定义的伽马校正", - "feature.linear_lighting.key_feature_2": "修正光照计算", - "feature.linear_lighting.key_feature_3": "使PBR真正生效", - "feature.linear_lighting.light_gamma": "光照伽马", - "feature.linear_lighting.membrane_effects_multiplier": "膜效果倍率", - "feature.linear_lighting.multipliers": "倍率", - "feature.linear_lighting.name": "线性光照", - "feature.linear_lighting.other_effects_multiplier": "其他效果倍率", - "feature.linear_lighting.point_light_multiplier": "点光源倍率", - "feature.linear_lighting.projected_effects_multiplier": "投射效果倍率", - "feature.linear_lighting.sky_gamma": "天空伽马", - "feature.linear_lighting.tab_advanced": "高级", - "feature.linear_lighting.tab_general": "通用", - "feature.linear_lighting.vanilla_diffuse_color_multiplier": "原版漫反射颜色倍率", - "feature.linear_lighting.vl_gamma": "体积光照伽马", - "feature.linear_lighting.water_gamma": "水伽马", - "feature.lod_blending.description": "在LOD对象与全细节对象之间提供无缝的视觉过渡,消除生硬的切换,创造平滑的视觉连续性。", - "feature.lod_blending.disable_terrain_vertex_colors": "禁用地形顶点颜色", - "feature.lod_blending.disable_terrain_vertex_colors_tooltip": "禁用附近地形上的顶点着色。建议与 xLODGen 生成、且 Vertex Color Intensity 设为 0 的地形 LOD 搭配使用。", - "feature.lod_blending.key_feature_1": "平滑的LOD对象亮度混合", - "feature.lod_blending.key_feature_2": "增强的地形LOD外观匹配", - "feature.lod_blending.key_feature_3": "针对雪景的LOD亮度调整", - "feature.lod_blending.key_feature_4": "可选的地形顶点颜色修改", - "feature.lod_blending.key_feature_5": "细节级别之间的无缝过渡", - "feature.lod_blending.lod_object_brightness": "LOD 物体亮度", - "feature.lod_blending.lod_object_gamma": "LOD 物体 Gamma", - "feature.lod_blending.lod_object_snow_brightness": "LOD 雪地物体亮度", - "feature.lod_blending.lod_object_snow_gamma": "LOD 雪地物体 Gamma", - "feature.lod_blending.lod_terrain_brightness": "LOD 地形亮度", - "feature.lod_blending.lod_terrain_gamma": "LOD 地形 Gamma", - "feature.lod_blending.name": "LOD混合", - "feature.perf_overlay.appearance": "外观", - "feature.perf_overlay.bg_opacity": "背景不透明度", - "feature.perf_overlay.clear_test_data": "清除测试数据", - "feature.perf_overlay.display_options": "显示选项", - "feature.perf_overlay.fps": "FPS:", - "feature.perf_overlay.frame_history_size": "帧历史大小", - "feature.perf_overlay.overlay_title": "性能叠加层", - "feature.perf_overlay.position": "位置:", - "feature.perf_overlay.post_fg_calculated": "帧生成后:计算计时(2倍帧生成前)", - "feature.perf_overlay.post_fg_fps": "帧生成后FPS:", - "feature.perf_overlay.post_fg_graph_tooltip": "FSR帧生成使用计算计时数据(2倍帧生成前)。\nDLSS帧生成提供测量计时数据。", - "feature.perf_overlay.raw_fps": "原始FPS:", - "feature.perf_overlay.reset_position": "重置位置", - "feature.perf_overlay.restore_defaults": "恢复默认值", - "feature.perf_overlay.restore_defaults_tooltip": "将性能叠加层设置恢复为默认值,包括图表、外观和更新间隔。", - "feature.perf_overlay.show_border": "显示边框", - "feature.perf_overlay.show_cs_passes": "显示CS渲染通道", - "feature.perf_overlay.show_draw_calls": "显示绘制调用", - "feature.perf_overlay.show_fps": "显示FPS计数器", - "feature.perf_overlay.show_frametime_graph": "显示帧时间图表", - "feature.perf_overlay.show_in_overlay": "在叠加层中显示", - "feature.perf_overlay.show_in_overlay_tooltip": "在单独的窗口中打开性能叠加层,即使主菜单关闭也保持打开。", - "feature.perf_overlay.show_post_fg_graph": "显示帧生成后帧时间图表", - "feature.perf_overlay.show_pre_fg_graph": "显示帧生成前帧时间图表", - "feature.perf_overlay.show_vram": "显示VRAM使用量", - "feature.perf_overlay.text_size": "文本大小", - "feature.perf_overlay.toggle_with": "切换键:", - "feature.perf_overlay.update_interval": "更新间隔", - "feature.perf_overlay.vram_not_available": "VRAM使用量:不可用", - "feature.perf_overlay.vram_usage": "VRAM使用量:", - "feature.performance_overlay.description": "实时性能监控系统,显示FPS、帧时间、绘制调用、显存使用量以及详细的着色器性能分析。", - "feature.performance_overlay.key_feature_1": "实时FPS和帧时间监控,可配置更新间隔", - "feature.performance_overlay.key_feature_2": "交互式绘制调用分析,按着色器类型展示性能细分", - "feature.performance_overlay.key_feature_3": "显存使用量监控,带可视化进度条", - "feature.performance_overlay.key_feature_4": "帧时间图表,用于帧生成前后的分析", - "feature.performance_overlay.key_feature_5": "A/B测试支持,对比不同配置的性能表现", - "feature.performance_overlay.key_feature_6": "颜色编码的性能指标,可自定义阈值", - "feature.performance_overlay.key_feature_7": "可移动的叠加窗口,位置持久保存", - "feature.performance_overlay.name": "性能叠加层", - "feature.render_doc.description": "提供应用内的RenderDoc捕获支持与便捷UI。", - "feature.render_doc.key_feature_1": "为捕获添加注释,可在RenderDoc UI中查看", - "feature.render_doc.key_feature_2": "打开捕获文件夹", - "feature.render_doc.key_feature_3": "捕获文件管理", - "feature.render_doc.name": "RenderDoc", - "feature.renderdoc.cancel": "取消", - "feature.renderdoc.capture_active": "RenderDoc捕获正在进行。", - "feature.renderdoc.capture_control": "捕获控制", - "feature.renderdoc.capture_control_tooltip": "手动捕获创建和基本控制", - "feature.renderdoc.capture_dir": "捕获目录:%s", - "feature.renderdoc.capture_dir_tooltip": "右键点击复制目录路径。", - "feature.renderdoc.capture_files": "捕获文件", - "feature.renderdoc.capture_files_tooltip": "查看和管理单个捕获文件", - "feature.renderdoc.capture_frames": "捕获帧数", - "feature.renderdoc.capture_frames_tooltip": "要捕获的连续帧数。1使用普通RenderDoc捕获;更高值使用TriggerMultiFrameCapture。", - "feature.renderdoc.capture_size": "捕获大小", - "feature.renderdoc.capture_size_tooltip": "捕获目录中所有捕获文件的总大小", - "feature.renderdoc.clear_all_captures": "清除所有捕获", - "feature.renderdoc.col_created": "创建时间", - "feature.renderdoc.col_filename": "文件名", - "feature.renderdoc.col_size": "大小", - "feature.renderdoc.comments_hint": "下一次捕获的附加注释(可选)", - "feature.renderdoc.comments_tooltip": "附加注释将追加到自动元数据中,并嵌入到.rdc文件中", - "feature.renderdoc.confirm_delete": "您确定要删除所有捕获文件吗?", - "feature.renderdoc.copy_dir_path": "复制目录路径", - "feature.renderdoc.create_capture": "创建捕获", - "feature.renderdoc.delete_size": "这将永久删除%u MB的捕获数据。", - "feature.renderdoc.disk_usage": "磁盘使用量", - "feature.renderdoc.disk_usage_tooltip": "监控捕获存储使用情况", - "feature.renderdoc.double_click_hint": "双击文件名以打开捕获文件", - "feature.renderdoc.enable_capture": "启用RenderDoc捕获", - "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", - "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", - "feature.renderdoc.hover_hint": "悬停在文件名上查看文件详情", - "feature.renderdoc.no_files": "未找到捕获文件。", - "feature.renderdoc.not_enough_space": "没有足够的可用磁盘空间来创建捕获。", - "feature.renderdoc.ok": "确定", - "feature.renderdoc.open_capture_dir": "打开捕获目录", - "feature.renderdoc.refresh_list": "刷新列表", - "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", - "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", - "feature.renderdoc.space_required": "至少需要{} MB的可用空间。", - "feature.renderdoc.yes_delete": "是,全部删除", - "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", - "feature.screen_space_gi.key_feature_1": "逼真的间接光照", - "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", - "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", - "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", - "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", - "feature.screen_space_gi.name": "屏幕空间GI", - "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", - "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", - "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", - "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", - "feature.screen_space_shadows.enable": "启用", - "feature.screen_space_shadows.enable_tooltip": "启用来自太阳/月亮方向的屏幕空间接触阴影。", - "feature.screen_space_shadows.general": "通用", - "feature.screen_space_shadows.key_feature_1": "增强的接触阴影", - "feature.screen_space_shadows.key_feature_2": "提升阴影细节", - "feature.screen_space_shadows.key_feature_3": "更好的阴影精度", - "feature.screen_space_shadows.key_feature_4": "精细尺度的阴影效果", - "feature.screen_space_shadows.key_feature_5": "可配置的阴影对比度", - "feature.screen_space_shadows.name": "屏幕空间阴影", - "feature.screen_space_shadows.sample_count": "采样数量倍率", - "feature.screen_space_shadows.sample_count_tooltip": "阴影射线采样数量的倍率。较高值以性能为代价增加阴影范围。适应渲染分辨率。", - "feature.screen_space_shadows.shadow_contrast": "阴影对比度", - "feature.screen_space_shadows.shadow_contrast_tooltip": "阴影过渡的对比度增强。较高值产生更硬的阴影边缘。", - "feature.screen_space_shadows.surface_thickness": "表面厚度", - "feature.screen_space_shadows.surface_thickness_tooltip": "阴影检测的假设表面厚度。较低值产生更薄、更精确的阴影。", - "feature.screen_space_shadows.vr_stereo_sync": "VR立体同步", - "feature.screen_space_shadows.vr_stereo_sync_tooltip": "通过双向重投影同步左右眼之间的阴影数据,并应用深度加权模糊以减少每只眼睛的噪点。使用最小混合,如果任一眼睛检测到遮挡物,则保留阴影。", - "feature.screenshot.apply_crop": "应用裁剪", - "feature.screenshot.async_note": "捕获和保存异步运行,不会让游戏停顿。", - "feature.screenshot.crop": "裁剪", - "feature.screenshot.folder": "文件夹", - "feature.screenshot.folder_tooltip": "相对路径相对于Skyrim安装目录解析。\n绝对路径(例如D:\\Captures)直接保存到该位置。", - "feature.screenshot.hdr_bit_depth": "HDR PNG 位深度", - "feature.screenshot.hdr_bit_depth_tooltip": "48 bpp RGB PNG 负载的量化位深。11位是较好的默认值;更高的值会增加文件大小,但收益递减。", - "feature.screenshot.hdr_note": "HDR 已启用:将显示帧保存为带有 HDR10 元数据的 PNG(48 bpp RGB,cICP/cLLi)。请使用支持 HDR 的查看器,如 Windows 照片(HDR 开启)或 Special K SKIF。", - "feature.screenshot.hotkey": "热键", - "feature.screenshot.hotkey_collision": "此热键与原版PrintScreen冲突;两者都会触发保存。在Skyrim.ini中设置bAllowScreenShot=0以抑制原版,或在上方选择不同的热键。", - "feature.screenshot.name": "截图", - "feature.screenshot.open": "打开", - "feature.screenshot.output": "输出", - "feature.screenshot.sdr_note": "启用HDR显示来捕捉有着HDR10元数据的HDR PNG截图。SDR和VR截图会使用以下选择的无损格式。", - "feature.screenshot.take_screenshot": "立即截图", - "feature.sky_sync.custom_angle": "自定义角度", - "feature.sky_sync.custom_angle_tooltip": "设置太阳轨迹的自定义角度。", - "feature.sky_sync.description": "将体积光照和阴影与天空中太阳和月亮的实际位置同步。", - "feature.sky_sync.enabled": "启用", - "feature.sky_sync.enabled_tooltip": "启用或禁用天空同步功能。", - "feature.sky_sync.key_feature_1": "修复太阳/月亮位置与光照方向不匹配的问题", - "feature.sky_sync.key_feature_2": "包含可配置的替代太阳路径,呈现更逼真戏剧化的光照", - "feature.sky_sync.key_feature_3": "根据可见性在太阳和月亮之间平滑切换光源", - "feature.sky_sync.key_feature_4": "月光源可在Masser、Secunda或最亮者之间切换", - "feature.sky_sync.key_feature_5": "基于月相自动计算月光强度", - "feature.sky_sync.key_feature_6": "修复玩家提升海拔时太阳在地平线上显得更高的问题", - "feature.sky_sync.min_shadow_elevation": "最小阴影仰角", - "feature.sky_sync.min_shadow_elevation_tooltip": "阳光设置的最小角度。限制阴影长度。更高 = 日落/日出时更短的阴影。", - "feature.sky_sync.moon_light_source": "月亮光源", - "feature.sky_sync.moon_light_source_brightest": "最亮者", - "feature.sky_sync.moon_light_source_masser": "Masser", - "feature.sky_sync.moon_light_source_secunda": "Secunda", - "feature.sky_sync.moon_light_source_tooltip": "选择夜晚投射阴影的月亮。", - "feature.sky_sync.name": "天空同步", - "feature.sky_sync.sun_path": "太阳路径", - "feature.sky_sync.sun_path_custom": "自定义", - "feature.sky_sync.sun_path_northern": "北侧天空", - "feature.sky_sync.sun_path_southern": "南侧天空", - "feature.sky_sync.sun_path_tooltip": "选择太阳穿越天空的轨迹。", - "feature.sky_sync.sun_path_vanilla": "原版", - "feature.sky_sync.sun_position_offsets": "太阳位置偏移", - "feature.sky_sync.sun_position_offsets_desc": "在日出/日落时移动太阳高度。重置天气以查看更改。", - "feature.sky_sync.sunrise_begin": "日出开始(小时)", - "feature.sky_sync.sunrise_begin_tooltip": "太阳开始升起的时间偏移。", - "feature.sky_sync.sunrise_end": "日出结束(小时)", - "feature.sky_sync.sunrise_end_tooltip": "太阳完成升起的时间偏移。", - "feature.sky_sync.sunset_begin": "日落开始(小时)", - "feature.sky_sync.sunset_begin_tooltip": "太阳开始落下的时间偏移。", - "feature.sky_sync.sunset_end": "日落结束(小时)", - "feature.sky_sync.sunset_end_tooltip": "太阳完成落下的时间偏移。", - "feature.sky_sync.use_alternate_sun_path": "使用备用太阳路径", - "feature.sky_sync.use_alternate_sun_path_tooltip": "根据时间和季节计算太阳位置,而非原版运动。", - "feature.skylighting.description": "通过计算天空遮蔽和方向光照,模拟逼真的环境照明,在户外环境中提供更精确自然的照明。", - "feature.skylighting.diffuse_min_visibility": "漫反射最小可见度", - "feature.skylighting.key_feature_1": "天空遮蔽计算,用于环境光照", - "feature.skylighting.key_feature_2": "基于环境几何体的方向性天空光照", - "feature.skylighting.key_feature_3": "增强的户外场景环境照明", - "feature.skylighting.key_feature_4": "支持变化的天空光照强度", - "feature.skylighting.key_feature_5": "与现有光照系统集成", - "feature.skylighting.max_zenith": "最大天顶角", - "feature.skylighting.max_zenith_tooltip": "较小的角度产生更集中的自上而下阴影。", - "feature.skylighting.min_visibility_desc": "最小可见度值。漫反射使物体变暗。镜面反射从反射中移除天空。", - "feature.skylighting.name": "天空光照", - "feature.skylighting.rebuild": "重建天光", - "feature.skylighting.rebuild_tooltip": "以下更改需要重建、加载屏幕或离开当前位置才能应用。", - "feature.skylighting.specular_min_visibility": "镜面反射最小可见度", - "feature.ssgi.ao_only": "仅AO", - "feature.ssgi.ao_power": "AO强度", - "feature.ssgi.ao_radius": "AO半径", - "feature.ssgi.ao_radius_tooltip": "较小的半径产生更紧密的AO。", - "feature.ssgi.blur": "模糊", - "feature.ssgi.blur_radius": "模糊半径", - "feature.ssgi.buffer_viewer": "缓冲区查看器", - "feature.ssgi.debug": "调试", - "feature.ssgi.denoising": "降噪", - "feature.ssgi.depth_fade_range": "深度渐隐范围", - "feature.ssgi.depth_fade_range_tooltip": "基于深度的效果渐隐的距离范围。", - "feature.ssgi.enabled": "启用", - "feature.ssgi.enabled_tooltip": "启用屏幕空间全局光照。禁用时,所有其他设置将被忽略。", - "feature.ssgi.extreme": "极致", - "feature.ssgi.extreme_tooltip": "全分辨率且干净。", - "feature.ssgi.full_res": "全分辨率", - "feature.ssgi.geometry_weight": "几何权重", - "feature.ssgi.geometry_weight_tooltip": "较高值使模糊对几何差异更敏感。", - "feature.ssgi.half_res": "半分辨率", - "feature.ssgi.hq_specular_il": "(实验性)HQ高光IL", - "feature.ssgi.hq_specular_il_tooltip": "实验性的高光GI,更准确但需要更多采样。不会被模糊。", - "feature.ssgi.il_distance_compensation": "IL距离补偿", - "feature.ssgi.il_distance_compensation_tooltip": "增亮/调暗更远的辐射度采样。", - "feature.ssgi.il_radius": "IL半径", - "feature.ssgi.il_radius_tooltip": "较大的半径产生更宽的IL。", - "feature.ssgi.il_saturation": "IL饱和度", - "feature.ssgi.il_source_brightness": "IL源亮度", - "feature.ssgi.indirect_lighting": "间接光照(IL)", - "feature.ssgi.low": "低", - "feature.ssgi.low_tooltip": "四分之一分辨率且模糊。", - "feature.ssgi.max_frame_accumulation": "最大帧累积", - "feature.ssgi.max_frame_accumulation_tooltip": "累积多少过去帧的结果。较高值噪点更少但可能导致鬼影。", - "feature.ssgi.min_screen_radius": "最小屏幕半径", - "feature.ssgi.min_screen_radius_tooltip": "以显示宽度比例表示的最小屏幕空间效果半径,防止远场AO过小。", - "feature.ssgi.movement_disocclusion": "运动去遮挡", - "feature.ssgi.movement_disocclusion_tooltip": "如果像素从上帧移动得太远,其辐射度将不会带入此帧。\n较低值更严格。", - "feature.ssgi.quality_performance": "质量/性能", - "feature.ssgi.quarter_res": "四分之一分辨率", - "feature.ssgi.reference": "参考", - "feature.ssgi.reference_tooltip": "参考模式。", - "feature.ssgi.shader_compile_error": "计算着色器编译失败!", - "feature.ssgi.show_advanced": "显示高级选项", - "feature.ssgi.slices": "切片", - "feature.ssgi.slices_tooltip": "采样采用多少个方向。\n控制噪点。", - "feature.ssgi.standard": "标准", - "feature.ssgi.standard_tooltip": "半分辨率且相对稳定。", - "feature.ssgi.steps_per_slice": "每切片步数", - "feature.ssgi.steps_per_slice_tooltip": "在每个方向上采样的数量。\n控制光照精度,以及效果半径较大时的噪点。", - "feature.ssgi.temporal_denoiser": "时间降噪器", - "feature.ssgi.thickness": "厚度", - "feature.ssgi.thickness_tooltip": "遮挡物的厚度。仅影响AO。", - "feature.ssgi.toggles": "开关", - "feature.ssgi.vanilla_ssao": "原版SSAO", - "feature.ssgi.vanilla_ssao_tooltip": "启用Skyrim内置的SSAO。使用SSGI时通常禁用此选项以避免双重变暗。", - "feature.ssgi.vanilla_ssao_tooltip_vr": "VR不支持原版SSAO。", - "feature.ssgi.view_resize": "视图调整大小", - "feature.ssgi.visual": "视觉", - "feature.ssgi.visual_il": "视觉 - IL", - "feature.sss.base_profile": "基础预设", - "feature.sss.blur_radius": "模糊半径", - "feature.sss.blur_radius_tooltip": "模糊半径。", - "feature.sss.burley": "Burley", - "feature.sss.burley_samples": "Burley采样数", - "feature.sss.enable_character_lighting": "启用角色光照", - "feature.sss.enable_character_lighting_tooltip": "原版功能,不推荐。", - "feature.sss.falloff": "衰减", - "feature.sss.human_profile": "人类预设", - "feature.sss.mean_free_path_color": "平均自由路径颜色", - "feature.sss.mean_free_path_color_tooltip": "控制光在红色、绿色和蓝色通道中进入次表面的距离。由平均自由路径距离缩放。", - "feature.sss.mean_free_path_distance": "平均自由路径距离", - "feature.sss.mean_free_path_distance_tooltip": "控制平均自由路径颜色进入次表面的距离。", - "feature.sss.separable_sss": "可分离SSS", - "feature.sss.settings": "设置", - "feature.sss.strength": "强度", - "feature.sss.thickness": "厚度", - "feature.sss.thickness_tooltip": "相对于深度的模糊半径。", - "feature.subsurface_scattering.description": "模拟光线穿透半透明材质(如皮肤),创造更逼真的角色光照效果。\n该技术使有机材质看起来更生动自然。", - "feature.subsurface_scattering.key_feature_1": "逼真的皮肤光照", - "feature.subsurface_scattering.key_feature_2": "光穿透模拟", - "feature.subsurface_scattering.key_feature_3": "为不同材质提供独立的配置文件", - "feature.subsurface_scattering.key_feature_4": "增强的角色外观", - "feature.subsurface_scattering.key_feature_5": "可配置的散射属性", - "feature.subsurface_scattering.name": "次表面散射", - "feature.terrain_blending.description": "提供地形与物体之间的无缝混合,消除物体与地面交汇处的生硬过渡,呈现更自然的景观。", - "feature.terrain_blending.enable": "启用地形混合", - "feature.terrain_blending.enable_tooltip": "启用地形与物体之间的无缝混合。", - "feature.terrain_blending.key_feature_1": "地形与物体的无缝混合过渡", - "feature.terrain_blending.key_feature_2": "高级深度缓冲区处理,实现平滑集成", - "feature.terrain_blending.key_feature_3": "支持替代地形渲染模式", - "feature.terrain_blending.key_feature_4": "针对复杂场景的多通道渲染优化", - "feature.terrain_blending.key_feature_5": "增强的地形交互视觉连续性", - "feature.terrain_blending.name": "地形混合", - "feature.terrain_helper.description": "为需要额外纹理槽和视差映射功能的地形模组提供增强的地形材质支持。", - "feature.terrain_helper.key_feature_1": "扩展的地形材质纹理槽支持", - "feature.terrain_helper.key_feature_2": "地形纹理的视差映射集成", - "feature.terrain_helper.key_feature_3": "自动地形材质检测与设置", - "feature.terrain_helper.key_feature_4": "支持高级地形修改", - "feature.terrain_helper.key_feature_5": "地形增强模组的兼容层", - "feature.terrain_helper.name": "地形辅助", - "feature.terrain_shadows.buffer_viewer": "缓冲区查看器", - "feature.terrain_shadows.debug": "调试", - "feature.terrain_shadows.description": "使用高度图数据为地形特征添加逼真的阴影投射,创造准确的地形阴影,增强深度感知和视觉真实感。", - "feature.terrain_shadows.enable_terrain_shadow": "启用地形阴影", - "feature.terrain_shadows.key_feature_1": "基于高度图的地形阴影计算", - "feature.terrain_shadows.key_feature_2": "基于太阳位置的动态阴影更新", - "feature.terrain_shadows.key_feature_3": "支持自定义高度图文件", - "feature.terrain_shadows.key_feature_4": "实时阴影预处理和计算", - "feature.terrain_shadows.key_feature_5": "与现有阴影系统集成", - "feature.terrain_shadows.name": "地形阴影", - "feature.terrain_variation.apply_to_lod_terrain": "应用到 LOD 地形", - "feature.terrain_variation.apply_to_lod_terrain_tooltip": "将该平铺修复应用到 LOD 地形对象。\n这有助于减少远处地形上可见的重复平铺效果。", - "feature.terrain_variation.description": "减少地形纹理的重复图案效果。\n通过为纹理采样添加变化来创造更自然的地形外观。", - "feature.terrain_variation.enable_tiling_fix": "启用地形平铺修复", - "feature.terrain_variation.enable_tiling_fix_tooltip": "减少地形纹理的重复平铺感。\n该技术通过在纹理采样中加入变化,让地形看起来更自然。", - "feature.terrain_variation.key_feature_1": "减少地形纹理的平铺感", - "feature.terrain_variation.key_feature_2": "可调节的基于距离的混合", - "feature.terrain_variation.key_feature_3": "提升地形视觉质量", - "feature.terrain_variation.key_feature_4": "与扩展材质视差兼容", - "feature.terrain_variation.name": "地形变化", - "feature.true_pbr.base_color_scale": "基础颜色缩放", - "feature.true_pbr.blue": "蓝", - "feature.true_pbr.coat": "镀层", - "feature.true_pbr.coat_color": "镀层颜色", - "feature.true_pbr.coat_roughness": "镀层粗糙度", - "feature.true_pbr.coat_specular_level": "镀层高光等级", - "feature.true_pbr.coat_strength": "镀层强度", - "feature.true_pbr.density_randomization": "密度随机化", - "feature.true_pbr.displacement_scale": "位移缩放", - "feature.true_pbr.enabled": "启用", - "feature.true_pbr.glint": "闪烁高光", - "feature.true_pbr.global_settings": "全局设置", - "feature.true_pbr.green": "绿", - "feature.true_pbr.inner_layer_displacement_offset": "内层位移偏移", - "feature.true_pbr.log_microfacet_density": "微表面密度对数", - "feature.true_pbr.material_density_randomization": "密度随机化", - "feature.true_pbr.material_glint": "闪烁高光", - "feature.true_pbr.material_glint_enabled": "启用", - "feature.true_pbr.material_log_microfacet_density": "微表面密度对数", - "feature.true_pbr.material_microfacet_roughness": "微表面粗糙度", - "feature.true_pbr.material_object": "材质对象", - "feature.true_pbr.material_object_settings": "材质对象设置", - "feature.true_pbr.material_save": "保存", - "feature.true_pbr.material_screenspace_scale": "屏幕空间缩放", - "feature.true_pbr.material_specular_level": "高光等级", - "feature.true_pbr.microfacet_roughness": "微表面粗糙度", - "feature.true_pbr.name": "True PBR", - "feature.true_pbr.red": "红", - "feature.true_pbr.reset_to_1_0": "重置为 1.0", - "feature.true_pbr.roughness": "粗糙度", - "feature.true_pbr.roughness_scale": "粗糙度缩放", - "feature.true_pbr.save": "保存", - "feature.true_pbr.screenspace_scale": "屏幕空间缩放", - "feature.true_pbr.specular_level": "高光等级", - "feature.true_pbr.subsurface": "次表面", - "feature.true_pbr.subsurface_color": "次表面颜色", - "feature.true_pbr.subsurface_opacity": "次表面不透明度", - "feature.true_pbr.texture_set": "纹理集", - "feature.true_pbr.texture_set_settings": "纹理集设置", - "feature.true_pbr.vertex_ao_strength": "顶点 AO 强度", - "feature.unified_water.debug": "调试", - "feature.unified_water.description": "通过用LOD0(近景水面)替换远处水面瓦片,提供全面的水面LOD不匹配修复。", - "feature.unified_water.error_water_cache_generation_failed_for_worldspaces_check": "错误:%d 个世界空间的水面缓存生成失败。请检查安装和 CommunityShaders.log", - "feature.unified_water.generating_water_cache": "正在生成水面缓存:", - "feature.unified_water.key_feature_1": "统一远景和近景水面的外观,统一所有光照视觉效果。", - "feature.unified_water.key_feature_2": "彻底且根本地解决水面LOD不匹配问题。", - "feature.unified_water.key_feature_3": "提供水面几何渲染的后台系统,支持更高级的水面效果。", - "feature.unified_water.key_feature_4": "通过使用优化的远距离水面网格来提升原版性能。", - "feature.unified_water.name": "统一水面", - "feature.unified_water.regenerate_caches": "重新生成缓存", - "feature.unified_water.regenerate_flowmap": "重新生成流图", - "feature.unified_water.use_optimised_meshes": "使用优化网格", - "feature.unified_water.use_optimised_meshes_tooltip": "使用三角面数显著更低的网格以提升性能,视觉质量无损。\n仅影响新创建的水体 - 需要切换位置或重启游戏才能生效。", - "feature.upscaling.backend_diagnostics": "后端诊断", - "feature.upscaling.description": "先进的超分辨率和帧生成技术,提升游戏性能。", - "feature.upscaling.dlss_model_preset": "DLSS模型预设", - "feature.upscaling.dlss_model_preset_default": "默认", - "feature.upscaling.dlss_model_preset_j": "预设 J", - "feature.upscaling.dlss_model_preset_k": "预设 K", - "feature.upscaling.dlss_model_preset_l": "预设 L", - "feature.upscaling.dlss_model_preset_m": "预设 M", - "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", - "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", - "feature.upscaling.fps_limit": "FPS限制", - "feature.upscaling.fps_limit_tooltip_1": "设置帧率上限目标。", - "feature.upscaling.fps_limit_tooltip_2": "起始值设置为比刷新率低2-3 FPS(例如120 Hz为117)。", - "feature.upscaling.frame_generation": "帧生成", - "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", - "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", - "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", - "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", - "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", - "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", - "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", - "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", - "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", - "feature.upscaling.key_feature_1": "DLSS(深度学习超采样)支持", - "feature.upscaling.key_feature_2": "FSR(FidelityFX超分辨率)支持", - "feature.upscaling.key_feature_3": "TAA(时间抗锯齿)支持", - "feature.upscaling.key_feature_4": "支持的系统可启用帧生成", - "feature.upscaling.low_latency_boost": "低延迟增强", - "feature.upscaling.low_latency_boost_tooltip_1": "保持GPU时钟更高,避免低GPU负载时的延迟尖峰。", - "feature.upscaling.low_latency_boost_tooltip_2": "在帧时间跳跃时有帮助;但会增加功耗和发热。", - "feature.upscaling.low_latency_mode": "低延迟模式", - "feature.upscaling.low_latency_mode_tooltip_1": "通过将CPU工作更紧密地与GPU同步来减少输入延迟。", - "feature.upscaling.low_latency_mode_tooltip_2": "可能略微降低最大FPS,但通常感觉响应更快。", - "feature.upscaling.marker_optimization_unavailable": "标记优化不可用(PCL未加载)。", - "feature.upscaling.method": "方法", - "feature.upscaling.method_none": "无", - "feature.upscaling.method_taa": "TAA", - "feature.upscaling.name": "超分辨率", - "feature.upscaling.native_inputs": "原生输入", - "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", - "feature.upscaling.preset_balanced": "平衡", - "feature.upscaling.preset_dlaa": "DLAA", - "feature.upscaling.preset_native_aa": "原生抗锯齿", - "feature.upscaling.preset_performance": "性能", - "feature.upscaling.preset_quality": "质量", - "feature.upscaling.preset_ultra_performance": "超级性能", - "feature.upscaling.reflex_blocked_by_fg": "当DX12帧生成交换链激活时,Reflex不可用。", - "feature.upscaling.reflex_not_available": "Reflex不可用。请确保sl.reflex.dll存在并重启。", - "feature.upscaling.sharpness": "锐度", - "feature.upscaling.streamline_logging": "Streamline日志记录", - "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", - "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", - "feature.upscaling.upscale_preset": "升频预设", - "feature.upscaling.upscaling_intermediates": "升频中间结果", - "feature.upscaling.use_fps_limit": "使用FPS限制", - "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", - "feature.upscaling.use_fps_limit_tooltip_2": "相比无上限渲染可以降低延迟。", - "feature.upscaling.use_markers_to_optimize": "使用标记优化", - "feature.upscaling.use_markers_to_optimize_tooltip_1": "使用帧标记以实现更紧密的Reflex计时。", - "feature.upscaling.use_markers_to_optimize_tooltip_2": "先尝试开启;如果在您的设置上造成卡顿则关闭。", - "feature.upscaling.view_resize": "视图调整大小", - "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", - "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", - "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", - "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", - "feature.volumetric_lighting.exterior_depth": "室外深度", - "feature.volumetric_lighting.exterior_height": "室外高度", - "feature.volumetric_lighting.exterior_quality": "室外质量", - "feature.volumetric_lighting.exterior_width": "室外宽度", - "feature.volumetric_lighting.interior_depth": "室内深度", - "feature.volumetric_lighting.interior_height": "室内高度", - "feature.volumetric_lighting.interior_quality": "室内质量", - "feature.volumetric_lighting.interior_width": "室内宽度", - "feature.volumetric_lighting.key_feature_1": "逼真的光散射", - "feature.volumetric_lighting.key_feature_2": "上帝射线和大气效果", - "feature.volumetric_lighting.key_feature_3": "独立的室内/室外设置", - "feature.volumetric_lighting.key_feature_4": "可配置的质量等级", - "feature.volumetric_lighting.key_feature_5": "增强的大气沉浸感", - "feature.volumetric_lighting.name": "体积光照", - "feature.volumetric_lighting.quality_custom": "自定义", - "feature.volumetric_lighting.quality_high": "高", - "feature.volumetric_lighting.quality_low": "低", - "feature.volumetric_lighting.quality_medium": "中", - "feature.volumetric_shadows.description": "为粒子和贴花等效果提供降采样的VSM阴影贴图。\n以最小的性能影响改善透明对象上的阴影质量。", - "feature.volumetric_shadows.key_feature_1": "降采样的VSM阴影", - "feature.volumetric_shadows.key_feature_2": "高斯模糊滤波", - "feature.volumetric_shadows.key_feature_3": "多级联支持", - "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", - "feature.volumetric_shadows.name": "体积阴影", - "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", - "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", - "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", - "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", - "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", - "feature.vr.key_feature_5": "可配置的遮挡剔除参数", - "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "调试", - "feature.vr_stereo.debug_pom_depth": "调试POM深度", - "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", - "feature.vr_stereo.enable": "启用", - "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", - "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", - "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", - "feature.vr_stereo.full_blend_distance": "完全混合距离", - "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", - "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", - "feature.vr_stereo.off": "关闭", - "feature.vr_stereo.pom_depth_scale": "POM深度缩放", - "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", - "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", - "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", - "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", - "feature.water_effects.key_feature_1": "逼真的水面焦散", - "feature.water_effects.key_feature_2": "增强的水下光照", - "feature.water_effects.key_feature_3": "水面上的动态光影图案", - "feature.water_effects.key_feature_4": "提升水面视觉保真度", - "feature.water_effects.key_feature_5": "大气水下效果", - "feature.water_effects.name": "水面效果", - "feature.wetness_effects.advanced": "高级", - "feature.wetness_effects.breadth": "广度", - "feature.wetness_effects.chance": "概率", - "feature.wetness_effects.chance_tooltip": "实际产生飞溅和涟漪的雨滴比例。较高的值会增加效果密度,但对性能影响最小。", - "feature.wetness_effects.climate_arctic_detail_0": "寒冷干燥的气候,降水量极少。", - "feature.wetness_effects.climate_arctic_detail_1": "最大降水量:约1.08毫米/小时(小雨)", - "feature.wetness_effects.climate_arctic_detail_2": "倍率:湿润度0.5倍,积水0.3倍,转换0.5倍。", - "feature.wetness_effects.climate_arctic_detail_3": "雨滴:30%概率,网格3.5单位,间隔0.4秒。", - "feature.wetness_effects.climate_arctic_detail_4": "性能影响:极低", - "feature.wetness_effects.climate_arctic_effect_0": "缓慢湿润累积(0.5倍)", - "feature.wetness_effects.climate_arctic_effect_1": "极少量积水形成(0.3倍)", - "feature.wetness_effects.climate_arctic_effect_2": "缓慢天气转换(0.5倍)", - "feature.wetness_effects.climate_arctic_effect_3": "稀疏降水(30%概率)", - "feature.wetness_effects.climate_coastal_detail_0": "海洋性气候,降水量大且频繁。", - "feature.wetness_effects.climate_coastal_detail_1": "最大降水量:约8.06毫米/小时(大雨)", - "feature.wetness_effects.climate_coastal_detail_2": "倍率:湿润度1.5倍,积水1.7倍,转换1.7倍。", - "feature.wetness_effects.climate_coastal_detail_3": "雨滴:80%概率,网格2.5单位,间隔0.25秒。", - "feature.wetness_effects.climate_coastal_detail_4": "性能影响:中等", - "feature.wetness_effects.climate_coastal_effect_0": "快速湿润累积(1.5倍)", - "feature.wetness_effects.climate_coastal_effect_1": "增强积水形成(1.7倍)", - "feature.wetness_effects.climate_coastal_effect_2": "快速天气转换(1.7倍)", - "feature.wetness_effects.climate_coastal_effect_3": "频繁降雨事件(80%概率)", - "feature.wetness_effects.climate_legacy_detail_0": "Riverwood的原始雨水效果值,提供完全向后兼容。", - "feature.wetness_effects.climate_legacy_detail_1": "最大降水量:约0.66毫米/小时(极小雨)", - "feature.wetness_effects.climate_legacy_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", - "feature.wetness_effects.climate_legacy_detail_3": "雨滴:30%概率,网格4.0单位,间隔0.5秒。", - "feature.wetness_effects.climate_legacy_detail_4": "性能影响:极低(基准线)", - "feature.wetness_effects.climate_legacy_effect_0": "原始湿润累积(1.0倍)", - "feature.wetness_effects.climate_legacy_effect_1": "原始积水形成(1.0倍)", - "feature.wetness_effects.climate_legacy_effect_2": "原始天气转换(1.0倍)", - "feature.wetness_effects.climate_legacy_effect_3": "原始雨滴频率(1.0倍)", - "feature.wetness_effects.climate_monsoon_detail_0": "热带/季风气候,极端降水量。", - "feature.wetness_effects.climate_monsoon_detail_1": "最大降水量:约22毫米/小时(极端)", - "feature.wetness_effects.climate_monsoon_detail_2": "倍率:湿润度2.0倍,积水2.5倍,转换2.0倍。", - "feature.wetness_effects.climate_monsoon_detail_3": "雨滴:100%概率,网格2.0单位,间隔0.2秒。", - "feature.wetness_effects.climate_monsoon_detail_4": "天际的小雨将无法匹配湿润效果。", - "feature.wetness_effects.climate_monsoon_detail_5": "性能影响:高(可能影响GPU性能)", - "feature.wetness_effects.climate_monsoon_effect_0": "极速湿润累积(2.0倍)", - "feature.wetness_effects.climate_monsoon_effect_1": "最大积水形成(2.5倍)", - "feature.wetness_effects.climate_monsoon_effect_2": "极动态天气(2.0倍)", - "feature.wetness_effects.climate_monsoon_effect_3": "最大雨滴频率(100%概率)", - "feature.wetness_effects.climate_nordic_detail_0": "平衡的温带北欧气候。", - "feature.wetness_effects.climate_nordic_detail_1": "最大降水量:约3.35毫米/小时(中雨)", - "feature.wetness_effects.climate_nordic_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", - "feature.wetness_effects.climate_nordic_detail_3": "雨滴:100%概率,网格3.0单位,间隔1.0秒。", - "feature.wetness_effects.climate_nordic_detail_4": "性能影响:低", - "feature.wetness_effects.climate_nordic_effect_0": "标准湿润累积(1.0倍)", - "feature.wetness_effects.climate_nordic_effect_1": "标准积水形成(1.0倍)", - "feature.wetness_effects.climate_nordic_effect_2": "标准天气转换(1.0倍)", - "feature.wetness_effects.climate_nordic_effect_3": "中等雨滴频率(100%概率)", - "feature.wetness_effects.climate_preset": "气候预设", - "feature.wetness_effects.climate_preset_arctic": "北极苔原", - "feature.wetness_effects.climate_preset_arctic_desc": "寒冷干燥的北极气候(小雨)", - "feature.wetness_effects.climate_preset_coastal": "温带沿海", - "feature.wetness_effects.climate_preset_coastal_desc": "海洋性气候(大雨)", - "feature.wetness_effects.climate_preset_custom": "自定义", - "feature.wetness_effects.climate_preset_custom_desc": "用户自定义设置", - "feature.wetness_effects.climate_preset_legacy": "旧版", - "feature.wetness_effects.climate_preset_legacy_desc": "原始雨水效果值(极小雨)", - "feature.wetness_effects.climate_preset_monsoon": "季风/极端", - "feature.wetness_effects.climate_preset_monsoon_desc": "极端季风气候(暴雨)", - "feature.wetness_effects.climate_preset_nordic": "北欧(默认)", - "feature.wetness_effects.climate_preset_nordic_desc": "平衡的北欧气候(中雨)", - "feature.wetness_effects.climate_preset_unknown": "未知", - "feature.wetness_effects.current_climate_preset": "当前气候预设", - "feature.wetness_effects.climate_presets": "气候预设", - "feature.wetness_effects.custom_preset_tooltip_0": "自定义设置 - 您已修改预设值。", - "feature.wetness_effects.custom_preset_tooltip_1": "在上方选择一个预设以应用预定义的气候设置。", - "feature.wetness_effects.debug": "调试", - "feature.wetness_effects.description": "添加逼真的湿润效果,包括基于降雨的表面湿润、积水形成、岸边湿润以及动态雨滴效果,增强天气沉浸感。", - "feature.wetness_effects.effect_range": "效果范围", - "feature.wetness_effects.effect_range_tooltip": "雨滴效果的作用范围", - "feature.wetness_effects.effects": "效果:", - "feature.wetness_effects.enable_interior_exterior_override": "启用室内/室外覆写", - "feature.wetness_effects.enable_puddle_override": "启用积水覆写", - "feature.wetness_effects.enable_rain_override": "启用降雨覆写", - "feature.wetness_effects.enable_raindrop_effects": "启用雨滴效果", - "feature.wetness_effects.enable_ripples": "启用涟漪", - "feature.wetness_effects.enable_ripples_tooltip": "在积水上启用圆形涟漪,在较小程度上也在其他湿润表面上生效", - "feature.wetness_effects.enable_splashes": "启用飞溅", - "feature.wetness_effects.enable_splashes_tooltip": "在干燥表面上启用小型湿润飞溅效果。", - "feature.wetness_effects.enable_vanilla_ripples": "启用原版涟漪", - "feature.wetness_effects.enable_vanilla_ripples_controlled": "启用原版涟漪 - 由Splashes of Storms控制", - "feature.wetness_effects.enable_wetness": "启用湿润效果", - "feature.wetness_effects.enable_wetness_override": "启用湿润覆写", - "feature.wetness_effects.enable_wetness_tooltip": "在水边和下雨时启用表面湿润效果。", - "feature.wetness_effects.grid_size": "网格尺寸", - "feature.wetness_effects.grid_size_tooltip_0": "雨滴放置的空间网格尺寸(越小=更多网格单元,更高的GPU开销)", - "feature.wetness_effects.grid_size_tooltip_1": "这是对性能最敏感的选项。仅在需要更逼真效果时才降低此值。", - "feature.wetness_effects.interior_exterior_override_tooltip": "如果禁用,将仅使用室外值。", - "feature.wetness_effects.interval": "间隔", - "feature.wetness_effects.interval_tooltip": "检查雨滴效果的频率(越低越频繁,中等性能影响)", - "feature.wetness_effects.key_feature_1": "基于天气条件的动态表面湿润", - "feature.wetness_effects.key_feature_2": "逼真的积水形成与岸边湿润效果", - "feature.wetness_effects.key_feature_3": "带动画飞溅和涟漪的雨滴效果", - "feature.wetness_effects.key_feature_4": "可配置的湿润强度和天气转换速度", - "feature.wetness_effects.key_feature_5": "支持皮肤湿润和特定材质响应", - "feature.wetness_effects.lifetime": "生命周期", - "feature.wetness_effects.max_radius": "最大半径", - "feature.wetness_effects.meters_format": "{:.2f} 米", - "feature.wetness_effects.min_radius": "最小半径", - "feature.wetness_effects.min_rain_wetness": "最小降雨湿润度", - "feature.wetness_effects.min_rain_wetness_tooltip": "物体因雨水变湿的最小程度。", - "feature.wetness_effects.name": "湿润效果", - "feature.wetness_effects.open_weather_picker": "打开天气选择器", - "feature.wetness_effects.open_weather_picker_tooltip": "在 CS 实用工具中打开天气选择器", - "feature.wetness_effects.portion_of_grid_size": "作为网格尺寸的比例。", - "feature.wetness_effects.puddle_max_angle": "积水最大角度", - "feature.wetness_effects.puddle_max_angle_tooltip": "表面需要多平才能形成积水。", - "feature.wetness_effects.puddle_min_wetness": "积水最小湿润度", - "feature.wetness_effects.puddle_min_wetness_tooltip": "积水开始形成时的湿润度值。", - "feature.wetness_effects.puddle_radius": "积水半径", - "feature.wetness_effects.puddle_radius_tooltip": "用于确定积水大小和位置的半径", - "feature.wetness_effects.puddle_wetness": "积水湿润度", - "feature.wetness_effects.puddle_wetness_in_exterior": "积水湿润度 室内/室外", - "feature.wetness_effects.radius": "半径", - "feature.wetness_effects.rain_in_exterior": "降雨 室内/室外", - "feature.wetness_effects.rain_wetness": "降雨湿润度", - "feature.wetness_effects.raindrop_effects": "雨滴效果", - "feature.wetness_effects.raindrops": "雨滴", - "feature.wetness_effects.raindrops_help": "在每个间隔内,每个网格单元中放置一个雨滴。\n只有设定比例的雨滴会实际触发飞溅和涟漪。\n", - "feature.wetness_effects.rain_system_state": "雨水系统状态", - "feature.wetness_effects.ripples": "涟漪", - "feature.wetness_effects.shore_range": "岸边范围", - "feature.wetness_effects.shore_range_tooltip": "岸边湿润效果影响水体的最大距离", - "feature.wetness_effects.shore_wetness": "岸边湿润度", - "feature.wetness_effects.skin_wetness": "皮肤湿润度", - "feature.wetness_effects.skin_wetness_tooltip": "雨天时角色皮肤和头发的湿润程度。", - "feature.wetness_effects.splashes": "飞溅", - "feature.wetness_effects.strength": "强度", - "feature.wetness_effects.vanilla_ripples_tooltip_0": "启用默认涟漪(例如Ripples01)。", - "feature.wetness_effects.vanilla_ripples_tooltip_1": "禁用可能要到下次天气变化时才生效。", - "feature.wetness_effects.weather_transition_speed": "天气转换速度", - "feature.wetness_effects.weather_transition_speed_tooltip": "下雨时湿润效果出现的速度以及雨停后干燥的速度。", - "feature.wetness_effects.wetness_effects": "湿润效果", - "feature.wetness_effects.wetness_in_exterior": "湿润度 室内/室外", - "menu.advanced.active_shaders": "活跃着色器", - "menu.advanced.active_shaders_tooltip": "最近帧中使用过的着色器列表。在上方启用着色器拦截可使用热键循环浏览并拦截着色器进行调试。约1秒未使用的着色器将从此列表中移除。", - "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", - "menu.advanced.addresses": "地址", - "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", - "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", - "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", - "menu.advanced.avoid_flow_control": "避免流控制", - "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.background_compiler_threads": "后台编译器线程", - "menu.advanced.background_compiler_threads_tooltip": "游戏过程中用于编译着色器的线程数。默认为性能核心的一半,以避免影响渲染线程。较高值可更快完成编译,但可能导致卡顿。", - "menu.advanced.block_next": "拦截下一个:", - "menu.advanced.block_previous": "拦截上一个:", - "menu.advanced.blocked_shader": "已拦截:%s", - "menu.advanced.change_shader_block_next": "更改##ShaderBlockNext", - "menu.advanced.change_shader_block_prev": "更改##ShaderBlockPrev", - "menu.advanced.clear_shader_cache": "清除着色器缓存", - "menu.advanced.clear_shader_cache_tooltip": "从内存中清除所有已编译的着色器。下次使用时强制重新编译所有着色器。", - "menu.advanced.click_to_block": "左键点击拦截此着色器", - "menu.advanced.click_to_unblock": "左键点击取消拦截此着色器", - "menu.advanced.column_class": "类别", - "menu.advanced.column_class_tooltip": "着色器类别", - "menu.advanced.column_descriptor": "描述符", - "menu.advanced.column_descriptor_tooltip": "着色器描述符", - "menu.advanced.column_frame_pct": "帧百分比", - "menu.advanced.column_frame_pct_tooltip": "此帧中绘制调用的百分比", - "menu.advanced.column_key": "键", - "menu.advanced.column_key_tooltip": "着色器键", - "menu.advanced.column_type": "类型", - "menu.advanced.column_type_tooltip": "着色器类型", - "menu.advanced.compiler_threads": "编译器线程", - "menu.advanced.compiler_threads_tooltip": "启动时用于编译着色器的线程数。默认为所有逻辑核心减去一个以留出系统开销(包含E核)。较高值可更快完成编译,但可能降低系统响应性。", - "menu.advanced.compute": "计算", - "menu.advanced.compute_tooltip": "替换计算着色器。设为false时将禁用上述类型的自定义计算着色器。供开发者测试CS着色器是否与原版行为匹配。", - "menu.advanced.copy_info": "复制信息", - "menu.advanced.copy_info_tooltip": "将包含缓存路径的完整着色器信息复制到剪贴板", - "menu.advanced.copy_key": "复制键", - "menu.advanced.dump_ini_settings": "导出INI设置", - "menu.advanced.dump_shaders": "导出着色器", - "menu.advanced.dump_shaders_tooltip": "在启动时导出着色器。仅在逆向着色器时使用。普通用户无需此功能。", - "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", - "menu.advanced.enable_file_watcher": "启用文件监视器", - "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", - "menu.advanced.enable_shader_blocking": "启用着色器拦截", - "menu.advanced.enable_shader_blocking_tooltip": "启用热键以循环浏览并拦截单个着色器,用于调试目的。", - "menu.advanced.frame_annotations": "帧注释", - "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", - "menu.advanced.half_precision": "半精度(部分精度)", - "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.infinite_core_efficiency": "无限核心效率", - "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", - "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", - "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", - "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", - "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", - "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", - "menu.advanced.log_level": "日志级别", - "menu.advanced.log_level_critical": "严重", - "menu.advanced.log_level_debug": "调试", - "menu.advanced.log_level_err": "错误", - "menu.advanced.log_level_info": "信息", - "menu.advanced.log_level_off": "关闭", - "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", - "menu.advanced.log_level_trace": "跟踪", - "menu.advanced.log_level_warn": "警告", - "menu.advanced.makespan_label": "完工时间(T_p)", - "menu.advanced.makespan_metric": "完工时间(T_p):%s", - "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", - "menu.advanced.open_logs": "打开日志", - "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", - "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", - "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", - "menu.advanced.pixel": "像素", - "menu.advanced.pixel_tooltip": "替换像素着色器。设为false时将禁用上述类型的自定义像素着色器。供开发者测试CS着色器是否与原版行为匹配。", - "menu.advanced.press_key_shader_block_next": "按下任意键设置着色器拦截下一个...", - "menu.advanced.press_key_shader_block_prev": "按下任意键设置着色器拦截上一个...", - "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", - "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", - "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", - "menu.advanced.relative_bar_format": "{}({:.1f}%)", - "menu.advanced.relative_durations": "相对持续时间(归一化)", - "menu.advanced.replace_original_shaders": "替换原始着色器", - "menu.advanced.shader_blocking_active": "着色器拦截已激活", - "menu.advanced.shader_class_label": "类别:%s", - "menu.advanced.shader_compiler_stats": "着色器编译器:{}", - "menu.advanced.shader_debug_header": "着色器调试", - "menu.advanced.shader_defines": "着色器定义", - "menu.advanced.shader_defines_tooltip": "着色器编译器的定义。以分号\";\"分隔。用空格清除。更改后需重建着色器。计算着色器需要重启才能重新编译。", - "menu.advanced.shader_descriptor": "描述符:0x%X", - "menu.advanced.shader_row_tooltip": "类型:{}\n类别:{}\n描述符:0x{:X}\n键:{}\n\n{}", - "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", - "menu.advanced.shader_type_label": "类型:%s", - "menu.advanced.span_label": "跨度(S)", - "menu.advanced.span_metric": "跨度(S,最长):%s", - "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", - "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", - "menu.advanced.statistics": "统计", - "menu.advanced.stop_blocking": "停止拦截##Section", - "menu.advanced.tab_developer": "开发者", - "menu.advanced.tab_disable_at_boot": "启动时禁用", - "menu.advanced.tab_logging": "日志记录", - "menu.advanced.tab_shader_debug": "着色器调试", - "menu.advanced.tab_testing": "测试", - "menu.advanced.test_conditions": "测试条件", - "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", - "menu.advanced.vertex": "顶点", - "menu.advanced.vertex_tooltip": "替换顶点着色器。设为false时将禁用上述类型的自定义顶点着色器。供开发者测试CS着色器是否与原版行为匹配。", - "menu.advanced.work_label": "工作量(W)", - "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", - "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", - "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", - "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", - "menu.clear_shader_cache": "清除着色器缓存", - "menu.clear_shader_cache_tooltip": "清除着色器缓存和磁盘缓存(如果启用)。\n着色器缓存是在运行时替换原版着色器的已编译着色器集合。\n磁盘缓存是磁盘上已编译着色器的集合。清除后意味着着色器仅在游戏再次遇到它们时才重新编译。", - "menu.disable_at_boot_desc": "选择要在启动时禁用的功能。这与删除feature.ini文件相同。重新启用需要重启。", - "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", - "menu.faq.a2": "每个功能都可以在左侧边栏菜单中找到。点击任何功能即可访问其设置。大多数功能包含预设和详细的工具提示,帮助您了解每个设置的作用。", - "menu.faq.a3": "功能可能因硬件不兼容、依赖项缺失或与其他模组冲突而无法加载。请查看\"功能问题\"选项卡,了解有关任何有问题的功能的详细信息。", - "menu.faq.a4": "着色器失败通常由混合文件版本引起。请确保所有功能均为最新,并避免混合测试版本或过时版本的文件。请查看\"功能问题\"选项卡和/或Wiki了解更多信息。更新您的功能并移除任何过时的功能。", - "menu.faq.a5": "首先启用性能叠加层来监控您的FPS。考虑禁用屏幕空间GI等占用资源的功能或降低质量设置。\"显示\"选项卡还包含可以提升性能的升频选项。", - "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", - "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", - "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", - "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", - "menu.faq.q1": "什么是Community Shaders?", - "menu.faq.q2": "如何配置功能?", - "menu.faq.q3": "为什么有些功能无法加载?", - "menu.faq.q4": "编译时出现“着色器失败”?", - "menu.faq.q5": "如何提升性能?", - "menu.faq.q6": "Community Shaders与ENB兼容吗?", - "menu.faq.q7": "菜单热键无效!", - "menu.faq.q8": "我想帮助开发Community Shaders。", - "menu.faq.q9": "Community Shaders是开源的吗?", - "menu.faq.title": "常见问题解答", - "menu.features": "功能", - "menu.features.advanced": "高级", - "menu.features.also_feature": "另见:%s", - "menu.features.apply_override": "应用覆盖", - "menu.features.available_after_restart": "此功能将在重启后可用。", - "menu.features.boot_toggle_tooltip": "切换启动时加载功能。\n当前状态:%s\n需要重启才能使更改生效。\n禁用可消除性能影响。", - "menu.features.cannot_apply_overrides_scene": "在场景特定设置激活时无法应用覆盖。\n请先暂停此功能的场景设置。", - "menu.features.click_to_navigate": "点击导航到%s", - "menu.features.col_constrained_by": "受限于", - "menu.features.col_forced_to": "强制为", - "menu.features.col_impacted_feature": "受影响的功能", - "menu.features.col_setting": "设置", - "menu.features.constraints_explanation": "这些设置在其各自的功能菜单中因约束激活而被禁用。调整约束功能以移除它们。", - "menu.features.disabled": "禁用", - "menu.features.display": "显示", - "menu.features.dont_show_warning": "不再显示此警告", - "menu.features.download_link": "点击此处下载此功能({})", - "menu.features.download_tooltip": "从模组页面下载功能。", - "menu.features.enable_to_access_config": "启用在上述功能以访问其配置选项。", - "menu.features.enabled": "启用", - "menu.features.error_header": "错误", - "menu.features.feature_issues": "功能问题", - "menu.features.features": "功能", - "menu.features.general": "通用", - "menu.features.home": "主页", - "menu.features.no_settings_available": "此功能没有可用设置。", - "menu.features.ok_button": "确定", - "menu.features.pause_weather_overrides": "暂停天气覆盖", - "menu.features.pause_weather_tooltip": "临时禁用此功能的基于天气的设置调整。\n此状态不会被保存。", - "menu.features.profiling": "性能分析", - "menu.features.restore_defaults_tooltip": "恢复此功能的默认设置", - "menu.features.restore_override_tooltip": "从模组文件恢复原始覆盖设置。\n这将丢弃您的自定义设置并恢复为模组作者的推荐设置。", - "menu.features.scene_specific_settings": "场景特定设置", - "menu.features.select_feature_left": "请从左侧选择一个功能。", - "menu.features.select_item_left": "请从左侧选择一个项目。", - "menu.features.settings_adjusted_warning": "由于功能不兼容,您的部分设置已被自动调整。", - "menu.features.settings_hidden_disabled": "功能设置已隐藏,因为此功能在启动时被禁用。", - "menu.features.unloaded_features": "已卸载的功能", - "menu.footer.d3d12_swap_chain": "D3D12 交换链:{status}", - "menu.footer.game_version": "游戏版本:{runtime} {version}", - "menu.footer.gpu": "GPU:{name}", - "menu.home.active_constraints": "活跃设置约束", - "menu.home.click_to_navigate": "点击导航到{feature}", - "menu.home.consider_disabling_at_boot": "考虑在启动时禁用。", - "menu.home.constraint_header_constrained_by": "受限于", - "menu.home.constraint_header_forced_to": "强制为", - "menu.home.constraint_header_setting": "设置", - "menu.home.constraints_desc": "某些设置受其他功能约束。悬停在行上查看详情。", - "menu.home.dev_wiki": "开发者Wiki", - "menu.home.github": "GitHub", - "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", - "menu.home.join_discord": "加入我们的Discord", - "menu.home.nexus_mods": "Nexus Mods", - "menu.home.quick_links": "快速链接", - "menu.home.welcome": "欢迎使用Community Shaders {version}", - "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", - "menu.home.wiki": "Wiki", - "menu.issues.all_ini_loading": "所有功能INI文件加载成功。", - "menu.issues.cancel": "取消", - "menu.issues.cannot_be_undone": "此操作无法撤销!", - "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", - "menu.issues.cleanup_actions": "清理操作:", - "menu.issues.clear_issue_list": "清除问题列表", - "menu.issues.clear_issue_list_tooltip": "清除此问题列表(清理后有用)。", - "menu.issues.compilation_breaking_desc": "以下功能修改了核心着色器文件,必须通过模组管理器完全卸载。如果核心着色器被修改,仅删除INI文件不会修复编译错误。", - "menu.issues.compilation_breaking_header": "破坏编译的功能", - "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", - "menu.issues.core_feature_installed": "核心功能已安装", - "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", - "menu.issues.current_version": "当前版本:%s", - "menu.issues.delete": "删除", - "menu.issues.delete_confirm": "确定要删除功能'%s'的所有文件吗?", - "menu.issues.delete_files_tooltip": "删除与此功能关联的所有文件(INI、着色器等)", - "menu.issues.delete_unknown_tooltip": "删除此未知功能的文件。警告:如果此功能修改了核心着色器,删除可能无法修复编译问题。", - "menu.issues.download_tooltip": "下载 {name}", - "menu.issues.download_version_tooltip": "下载 {name} {version} 或更高版本", - "menu.issues.file_label": "文件:%s", - "menu.issues.files_label": "文件:", - "menu.issues.general_actions": "常规操作:", - "menu.issues.guidance_label": "指导:%s", - "menu.issues.hlsl_files_count": "%zu个HLSL文件", - "menu.issues.hlsl_files_found": "HLSL文件:找到%zu个", - "menu.issues.ini_file_label": "INI文件:%s", - "menu.issues.ini_label": "INI:%s", - "menu.issues.ini_path": "INI路径:%s", - "menu.issues.issue_label": "问题:%s", - "menu.issues.last_modified": "最后修改:", - "menu.issues.minimum_required": "最低要求:%s", - "menu.issues.no_issues": "未发现功能问题!", - "menu.issues.obsolete_compilation_failure": "此过时功能修改了核心着色器文件并导致编译失败。必须通过模组管理器卸载。", - "menu.issues.obsolete_features_desc": "以下功能已过时并已自动禁用。这些功能在此CS版本中已被移除或替换,但未修改核心着色器。", - "menu.issues.obsolete_features_header": "过时功能", - "menu.issues.open_features_folder": "打开功能文件夹", - "menu.issues.open_features_folder_tooltip": "打开包含INI文件的功能文件夹以供手动审查。", - "menu.issues.open_logs": "打开日志", - "menu.issues.open_logs_tooltip": "打开CommunityShaders.log文件以供手动审查。", - "menu.issues.open_shaders_directory": "打开着色器目录", - "menu.issues.open_shaders_tooltip": "打开主着色器目录以查看各个功能着色器文件夹。", - "menu.issues.override_failures_desc": "以下覆盖文件加载或应用失败。请检查文件格式和内容。", - "menu.issues.override_failures_header": "覆盖失败", - "menu.issues.potential_compilation_failure": "潜在的编译失败", - "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", - "menu.issues.replaced_by_prefix": "(被替换为", - "menu.issues.replaced_by_suffix": ")", - "menu.issues.replacement_label": "替代:%s", - "menu.issues.shader_directory_label": "着色器目录:%s", - "menu.issues.shader_folder": "着色器文件夹:%s", - "menu.issues.test.active_inis_count": "活动的测试 INI 文件({count}):\n", - "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", - "menu.issues.test.create_test_inis": "创建测试INI", - "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", - "menu.issues.test.feature_issue_testing": "功能问题测试", - "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", - "menu.issues.test.modified_notice": "\n部分测试文件已修改 - 建议恢复以清理", - "menu.issues.test.no_active_inis": "当前没有活跃的测试INI文件。", - "menu.issues.test.restore": "恢复", - "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", - "menu.issues.test.testing_header": "测试", - "menu.issues.this_will_delete": "这将删除:", - "menu.issues.time_label": "时间:%s", - "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", - "menu.issues.unknown_compilation_warning": "此未知功能可能修改了核心着色器文件,并可能导致编译失败。如果故障继续,应移除未知功能。", - "menu.issues.unknown_delete_warning": "这是一个未知功能。如果它修改了核心着色器文件(在其自身文件夹之外),仅删除这些文件不会修复着色器编译问题。", - "menu.issues.unknown_features_desc": "以下功能未被识别,我们已尝试自动禁用。它们可能来自开发分支或较新的CS版本。由于我们无法确定它们可能修改了哪些文件,应作为预防措施将其移除,以防止潜在的着色器编译失败。", - "menu.issues.unknown_features_header": "未知功能", - "menu.issues.update_no_link_tooltip": "此功能需要更新,但没有可用的下载链接。请手动检查模组页面。", - "menu.issues.update_required": "需要更新", - "menu.issues.update_to_version_required": "需要更新到 {version}+", - "menu.issues.use_clear_issue_list": "手动清理后使用\"清除问题列表\"刷新", - "menu.issues.use_open_features_folder": "使用\"打开功能文件夹\"手动审查INI文件", - "menu.issues.use_open_logs": "使用\"打开日志\"手动审查日志", - "menu.issues.use_open_shaders_directory": "使用\"打开着色器目录\"检查孤立着色器文件夹", - "menu.issues.warning_label": "警告:", - "menu.issues.wrong_version_desc": "以下功能存在版本兼容性问题,已自动禁用。请检查是否有更新或者该功能是否被视为过时。", - "menu.issues.wrong_version_header": "错误版本功能", - "menu.restore_settings": "恢复已保存的设置", - "menu.save_settings": "保存设置", - "menu.settings.auto_hide_feature_list": "自动隐藏功能列表", - "menu.settings.auto_hide_feature_list_tooltip": "自动隐藏左侧功能列表面板。将光标移到左边缘即可显示。", - "menu.settings.background_blur": "背景模糊", - "menu.settings.background_blur_tooltip": "对菜单窗口后面的背景应用模糊效果。", - "menu.settings.base_font_size": "基础字体大小", - "menu.settings.borders_and_separators": "边框和分隔符", - "menu.settings.button_text_align": "按钮文本对齐", - "menu.settings.button_text_align_tooltip": "当按钮大于其文本内容时应用对齐。", - "menu.settings.cancel": "取消", - "menu.settings.cell_padding": "单元格内边距", - "menu.settings.center_header_title": "居中标题", - "menu.settings.center_header_title_tooltip": "将Community Shaders标题和徽标在标题栏中居中", - "menu.settings.child_border_size": "子窗口边框大小", - "menu.settings.child_rounding": "子窗口圆角", - "menu.settings.color_background": "背景", - "menu.settings.color_border": "边框", - "menu.settings.color_border_shadow": "边框阴影", - "menu.settings.color_button": "按钮", - "menu.settings.color_button_active": "按钮(激活)", - "menu.settings.color_button_hovered": "按钮(悬停)", - "menu.settings.color_button_left": "左侧", - "menu.settings.color_button_position": "颜色按钮位置", - "menu.settings.color_button_right": "右侧", - "menu.settings.color_check_mark": "复选框勾选标记", - "menu.settings.color_child_bg": "子窗口背景", - "menu.settings.color_current_hotkey": "当前热键", - "menu.settings.color_default": "默认", - "menu.settings.color_disabled": "禁用", - "menu.settings.color_docking_empty_bg": "停靠空白背景", - "menu.settings.color_docking_preview": "停靠预览", - "menu.settings.color_drag_drop_target": "拖放目标", - "menu.settings.color_drag_drop_target_bg": "拖放目标背景", - "menu.settings.color_error": "错误", - "menu.settings.color_frame_bg": "框架背景", - "menu.settings.color_frame_bg_active": "框架背景(激活)", - "menu.settings.color_frame_bg_hovered": "框架背景(悬停)", - "menu.settings.color_header": "标题", - "menu.settings.color_header_active": "标题(激活)", - "menu.settings.color_header_hovered": "标题(悬停)", - "menu.settings.color_hovered": "悬停", - "menu.settings.color_info": "信息", - "menu.settings.color_input_text_cursor": "输入文本光标", - "menu.settings.color_menu_bar_bg": "菜单栏背景", - "menu.settings.color_minimized_transparency": "最小化透明度", - "menu.settings.color_modal_window_dim_bg": "模态窗模糊背景", - "menu.settings.color_nav_cursor": "导航光标", - "menu.settings.color_nav_windowing_dim_bg": "窗口导航模糊背景", - "menu.settings.color_nav_windowing_highlight": "窗口导航高亮", - "menu.settings.color_plot_histogram": "图表直方图", - "menu.settings.color_plot_histogram_hovered": "图表直方图(悬停)", - "menu.settings.color_plot_lines": "图表折线", - "menu.settings.color_plot_lines_hovered": "图表折线(悬停)", - "menu.settings.color_popup_bg": "弹出窗口背景", - "menu.settings.color_resize_grip": "调整大小手柄", - "menu.settings.color_resize_grip_active": "调整大小手柄(激活)", - "menu.settings.color_resize_grip_hovered": "调整大小手柄(悬停)", - "menu.settings.color_restart_needed": "需要重启", - "menu.settings.color_scrollbar_bg": "滚动条背景", - "menu.settings.color_scrollbar_grab": "滚动条滑块", - "menu.settings.color_scrollbar_grab_active": "滚动条滑块(激活)", - "menu.settings.color_scrollbar_grab_hovered": "滚动条滑块(悬停)", - "menu.settings.color_separator": "分隔符", - "menu.settings.color_separator_active": "分隔符(激活)", - "menu.settings.color_separator_hovered": "分隔符(悬停)", - "menu.settings.color_separator_line": "分隔线", - "menu.settings.color_slider_grab": "滑块手柄", - "menu.settings.color_slider_grab_active": "滑块手柄(激活)", - "menu.settings.color_slider_input_bg": "滑块和输入框背景", - "menu.settings.color_success": "成功", - "menu.settings.color_tab": "标签页", - "menu.settings.color_tab_dimmed": "标签页(变暗)", - "menu.settings.color_tab_dimmed_selected": "标签页(变暗选中)", - "menu.settings.color_tab_dimmed_selected_overline": "标签页变暗选中上划线", - "menu.settings.color_tab_hovered": "标签页(悬停)", - "menu.settings.color_tab_selected": "标签页(选中)", - "menu.settings.color_tab_selected_overline": "标签页选中上划线", - "menu.settings.color_table_border_light": "表格边框(浅色)", - "menu.settings.color_table_border_strong": "表格边框(深色)", - "menu.settings.color_table_header_bg": "表格标题背景", - "menu.settings.color_table_row_bg": "表格行背景", - "menu.settings.color_table_row_bg_alt": "表格行背景(交替)", - "menu.settings.color_text": "文本", - "menu.settings.color_text_disabled": "文本(禁用)", - "menu.settings.color_text_link": "文本链接", - "menu.settings.color_text_selected_bg": "文本选择背景", - "menu.settings.color_title_bg": "标题栏背景", - "menu.settings.color_title_bg_active": "标题栏背景(激活)", - "menu.settings.color_title_bg_collapsed": "标题栏背景(折叠)", - "menu.settings.color_tree_lines": "树状线条", - "menu.settings.color_unsaved_marker": "未保存标记", - "menu.settings.color_warning": "警告", - "menu.settings.color_window_bg": "窗口背景", - "menu.settings.color_window_border": "窗口边框", - "menu.settings.create_new_theme": "创建新主题", - "menu.settings.create_new_theme_hint": "使用当前设置创建新主题:", - "menu.settings.create_theme": "创建主题", - "menu.settings.delete_button": "删除", - "menu.settings.delete_theme": "删除主题", - "menu.settings.delete_theme_confirm_part1": "您确定要删除主题'", - "menu.settings.delete_theme_confirm_part2": "'?\n\n这将永久删除主题文件。此操作不可撤销。", - "menu.settings.delete_theme_title": "删除主题", - "menu.settings.delete_theme_tooltip": "删除'%s'的主题文件。此操作不可撤销。", - "menu.settings.description": "描述", - "menu.settings.description_tooltip": "主题的可选描述", - "menu.settings.display_name": "显示名称", - "menu.settings.display_name_duplicate": "已存在具有此显示名称的主题", - "menu.settings.display_name_tooltip": "在下拉菜单中显示的人类可读名称", - "menu.settings.docking_splitter_size": "停靠分隔器大小", - "menu.settings.effect_toggle_key": "效果切换键:", - "menu.settings.effective_size": "有效大小:%.0f px", - "menu.settings.enable_async": "启用异步", - "menu.settings.enable_async_tooltip": "如果着色器尚未编译则跳过替换。还会使编译速度极快!", - "menu.settings.enable_disk_cache": "启用磁盘缓存", - "menu.settings.enable_disk_cache_tooltip": "禁用从磁盘加载着色器,并阻止将已编译着色器保存到磁盘缓存。", - "menu.settings.feature_header_scale": "功能标题缩放", - "menu.settings.feature_header_scale_tooltip": "设置选项卡中功能标题文本的缩放倍率。", - "menu.settings.feature_headings": "功能标题", - "menu.settings.file_label": "文件:%s", - "menu.settings.filter_colors": "过滤颜色", - "menu.settings.font": "字体", - "menu.settings.font_roles": "字体角色", - "menu.settings.frame_border_size": "框架边框大小", - "menu.settings.frame_padding": "框架内边距", - "menu.settings.frame_rounding": "框架圆角", - "menu.settings.full_palette": "完整调色板", - "menu.settings.full_palette_tooltip": "用于详细自定义所有UI元素的高级颜色控制。", - "menu.settings.global_scale": "全局缩放", - "menu.settings.grab_min_size": "滑块最小大小", - "menu.settings.grab_rounding": "滑块圆角", - "menu.settings.indent_spacing": "缩进间距", - "menu.settings.item_inner_spacing": "项目内部间距", - "menu.settings.item_spacing": "项目间距", - "menu.settings.language": "语言", - "menu.settings.language_tooltip": "选择Community Shaders界面的显示语言。", - "menu.settings.last_shader_cache_duration": "上次着色器缓存构建持续时间:%s", - "menu.settings.log_slider_deadzone": "对数滑块死区", - "menu.settings.no_families": "无字体家族", - "menu.settings.no_font_families_available": "无可用字体家族", - "menu.settings.no_fonts_found": "未找到字体。请将.ttf文件放入Interface/CommunityShaders/Fonts/", - "menu.settings.no_style_variants": "未找到此字体家族的样式变体。", - "menu.settings.no_styles": "无样式", - "menu.settings.open_themes_folder": "打开主题文件夹", - "menu.settings.open_themes_folder_tooltip": "打开主题文件夹,您可以在其中添加自定义主题文件。", - "menu.settings.overlay_toggle_key": "叠加层切换键:", - "menu.settings.popup_border_size": "弹出窗口边框大小", - "menu.settings.popup_rounding": "弹出窗口圆角", - "menu.settings.refresh": "刷新", - "menu.settings.refresh_font_families": "刷新字体家族", - "menu.settings.refresh_font_families_tooltip": "添加或删除字体文件后重新扫描字体目录。", - "menu.settings.require_shift_to_dock": "需要Shift键停靠", - "menu.settings.require_shift_to_dock_tooltip": "启用时,拖动时必须按住Shift键才能停靠/对齐窗口。防止意外停靠。", - "menu.settings.reset": "重置", - "menu.settings.save_as_new_theme": "保存为新主题", - "menu.settings.save_theme_button": "保存", - "menu.settings.save_theme_tooltip": "使用当前设置更新当前选中的主题(%s)", - "menu.settings.screenshot_key": "截图键:", - "menu.settings.scrollbar_opacity": "滚动条不透明度", - "menu.settings.scrollbar_rounding": "滚动条圆角", - "menu.settings.scrollbar_size": "滚动条大小", - "menu.settings.section_borders": "边框", - "menu.settings.section_docking": "停靠", - "menu.settings.section_language": "语言", - "menu.settings.section_layout": "布局", - "menu.settings.section_main": "主页", - "menu.settings.section_rounding": "圆角", - "menu.settings.section_tables": "表格", - "menu.settings.section_widgets": "控件", - "menu.settings.selectable_text_align": "可选取文本对齐", - "menu.settings.selectable_text_align_tooltip": "当可选取项大于其文本内容时应用对齐。", - "menu.settings.selected_theme": "已选主题:", - "menu.settings.separator_text_align": "分隔符文本对齐", - "menu.settings.separator_text_border_size": "分隔符文本边框大小", - "menu.settings.separator_text_padding": "分隔符文本内边距", - "menu.settings.shader_deduplicated": "已去重", - "menu.settings.shader_disk_cache": "磁盘缓存", - "menu.settings.shader_failed": "失败", - "menu.settings.shader_fast": "快速(<2秒)", - "menu.settings.shader_slow": "慢速(2-8秒)", - "menu.settings.shader_very_slow": "非常慢(>=8秒)", - "menu.settings.show_footer": "显示页脚", - "menu.settings.show_footer_tooltip": "在窗口底部显示包含游戏版本、交换链和GPU信息的页脚", - "menu.settings.show_icon_buttons_in_header": "在标题栏中显示图标按钮", - "menu.settings.show_icon_buttons_in_header_tooltip": "启用时:在标题栏中将操作按钮(保存、加载、清除缓存)显示为图标\n禁用时:在标题栏下方显示为文本按钮", - "menu.settings.skip_clear_cache_dialogue": "跳过清除缓存对话框", - "menu.settings.skip_clear_cache_dialogue_tooltip": "勾选时,着色器缓存将立即清除,无需确认。", - "menu.settings.skip_compilation_key": "跳过编译键:", - "menu.settings.skip_unchanged_shaders": "跳过未更改的着色器", - "menu.settings.skip_unchanged_shaders_tooltip": "启用时,仅当每个着色器的.hlsl文件比磁盘上缓存的.bin更新时才从源代码重新编译。源文件未更改的着色器直接从磁盘缓存加载,避免完整的启动编译开销。适用于迭代测试:更改着色器文件后仅重建该着色器。需要\"启用磁盘缓存\"处于活动状态。", - "menu.settings.status": "状态", - "menu.settings.tab_bar_border_size": "标签栏边框大小", - "menu.settings.tab_behavior": "行为", - "menu.settings.tab_border_size": "标签页边框大小", - "menu.settings.tab_colors": "颜色", - "menu.settings.tab_fonts": "字体", - "menu.settings.tab_interface": "界面", - "menu.settings.tab_keybindings": "按键绑定", - "menu.settings.tab_rounding": "标签页圆角", - "menu.settings.tab_shaders": "着色器", - "menu.settings.tab_styling": "样式", - "menu.settings.tab_themes": "主题", - "menu.settings.table_angled_headers_angle": "表格斜角标题角度", - "menu.settings.theme_name": "主题名称", - "menu.settings.theme_name_duplicate": "已存在具有此名称的主题", - "menu.settings.theme_name_required": "主题名称为必填", - "menu.settings.theme_name_tooltip": "主题的文件名(不含.json扩展名)", - "menu.settings.theme_preset": "主题预设", - "menu.settings.theme_save_info": "主题更改不会随全局\"保存设置\"按钮保存。使用主题选项卡将更改保存到此主题。", - "menu.settings.theme_save_reminder": "如果您更改了上述主题,请使用全局\"保存设置\"按钮保存您的选择。", - "menu.settings.theme_update_failed": "更新主题失败", - "menu.settings.theme_updated_no_changes": "主题更新成功 - 未检测到更改", - "menu.settings.theme_updated_with_changes": "主题更新成功!更改的设置:", - "menu.settings.thumb_active_opacity": "滑块激活不透明度", - "menu.settings.thumb_active_opacity_tooltip": "控制滚动条滑块被拖动时的不透明度。", - "menu.settings.thumb_hovered_opacity": "滑块悬停不透明度", - "menu.settings.thumb_hovered_opacity_tooltip": "控制滚动条滑块悬停时的不透明度。", - "menu.settings.thumb_opacity": "滑块不透明度", - "menu.settings.thumb_opacity_tooltip": "控制滚动条滑块(可拖动的部分)的不透明度。", - "menu.settings.toggle_key": "切换键:", - "menu.settings.tooltip_hover_delay": "工具提示悬停延迟", - "menu.settings.tooltip_hover_delay_tooltip": "悬停在项目上时工具提示出现前等待的秒数。", - "menu.settings.track_opacity": "滚动轨道不透明度", - "menu.settings.track_opacity_tooltip": "控制滚动条轨道/通道(滚动条后面的背景区域)的不透明度。", - "menu.settings.ui_behavior": "UI行为", - "menu.settings.use_custom_shaders": "使用自定义着色器", - "menu.settings.use_custom_shaders_tooltip": "禁用此项实际上会禁用所有功能。", - "menu.settings.use_monochrome_cs_logo": "使用单色CS徽标", - "menu.settings.use_monochrome_cs_logo_tooltip": "使用Community Shaders徽标的单色版本", - "menu.settings.use_monochrome_icons": "使用单色图标", - "menu.settings.use_monochrome_icons_tooltip": "使用适应主题文本颜色的白色单色图标", - "menu.settings.use_resolution_based_font_size": "使用基于分辨率的字体大小", - "menu.settings.use_resolution_based_font_size_tooltip": "启用时,UI字体大小根据屏幕分辨率缩放。禁用以设置固定大小。", - "menu.settings.visual_effects": "视觉效果", - "menu.settings.window_border_size": "窗口边框大小", - "menu.settings.window_padding": "窗口内边距", - "menu.settings.window_rounding": "窗口圆角", - "menu.setup.change_later": "您可以稍后在通用 > 按键绑定中更改此项。", - "menu.setup.choose_hotkey": "请选择一个热键来访问菜单:", - "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", - "menu.setup.new_install_line2": "重新安装Community Shaders。", - "menu.setup.press_any_key": "按下任意键设置为切换键...", - "menu.setup.press_to_close": "按Escape或Enter继续", - "menu.toggle_error_message": "切换错误消息", - "menu.toggle_error_message_tooltip": "隐藏或显示着色器失败消息。您的安装已损坏,游戏中可能会看到错误。请仔细检查是否已更新所有功能以及加载顺序是否正确。请参阅CommunityShaders.log了解详情,并查看Nexus Mods页面或Discord服务器。", - "menu.window_title": "Community Shaders {version}", - "menu.window_title_dev": "Community Shaders {version} [{build}]", - "overlay.modified_features": "检测到可能修改了着色器的功能。请检查菜单中的功能问题。", - "overlay.shader_blocking_active": "着色器拦截已激活", - "overlay.uncompiled_warning": "警告:未编译的着色器在加载时会有视觉错误或导致卡顿。", - "ui.cancel": "取消", - "ui.clear_cache": "清除缓存", - "ui.clear_cache_confirm": "您确定要清除着色器缓存吗?", - "ui.clear_cache_desc": "这将清除内存和磁盘缓存(如果启用)中的所有已编译着色器。着色器将在游戏下次遇到它们时重新编译。", - "ui.clear_shader_cache": "清除着色器缓存?", - "ui.copy": "复制", - "ui.dont_ask_again": "不再提示", - "ui.search": "搜索...", - "ui.search_features": "搜索功能...", - "feature.cs_editor.description": "用于在游戏内检查、编辑和预览面向渲染器数据的开发工具。", - "feature.cs_editor.key_feature_1": "提供天气编辑功能", - "feature.cs_editor.key_feature_2": "包含对原版后处理和天气设置的动态保存与加载。", - "feature.cs_editor.key_feature_3": "实时编辑和预览效果", - "feature.cs_editor.key_feature_4": "即时切换任意天气,支持立即或渐变过渡", - "feature.cs_editor.key_feature_5": "按类型筛选天气(晴朗、多云、雨天、雪天、极光),方便浏览", - "feature.cs_editor.key_feature_6": "查看详细的天气信息,包括风、降水和闪电数据", - "feature.cs_editor.key_feature_7": "颜色编码的天气名称,一目了然地展示所有天气属性", - "feature.cs_editor.key_feature_8": "持久叠加窗口,可在游戏过程中持续监控天气", - "feature.cs_editor.name": "CS 编辑器", - "menu.settings.cs_editor_toggle_key": "CS 编辑器切换键:", - "menu.setup.cs_editor_unbound": "CS 编辑器热键未绑定 - 所选键使用 Shift", - "menu.setup.cs_editor_will_be": "CS 编辑器热键将为:{key}", - "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "原版镜面反射贴图倍率,应用到第一层粗糙度。", - "feature.skin.adds_a_constant_layer_of_wetness_to_all": "为所有皮肤添加一层恒定湿润度,使其始终略显潮湿或出汗,即使角色不在水中或没有剧烈活动。", - "feature.skin.advanced_skin_shader_using_dual_specular_lobes": "使用双镜面反射叶瓣的高级皮肤着色器。", - "feature.skin.base_color_multiplier": "基础色倍率", - "feature.skin.body_tiling_multiplier": "身体平铺倍率", - "feature.skin.controls_how_bumpy_wet_skin_appears_higher_values": "控制湿皮肤看起来有多凹凸。较高的值会让湿润区域出现更明显的表面波纹和扭曲。", - "feature.skin.controls_how_much_fine_detail_is_added_to": "控制添加到湿润图案中的细节量。较高的值会在基础图案上叠加更多小尺度变化。", - "feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer": "控制角质层的微观粗糙度。", - "feature.skin.controls_the_overall_contrast_and_roughness_of_the": "控制湿润图案的整体对比度和粗糙度。较高的值会让图案更明显、变化更多。", - "feature.skin.controls_the_size_of_the_wet_dry_pattern": "控制皮肤上干湿图案的大小。较高的值会产生更细、更详细的图案;较低的值会产生更大、更宽的湿斑。", - "feature.skin.description": "高级皮肤通过多种技术增强角色皮肤渲染。", - "feature.skin.dynamic_wetness_detected": "检测到动态湿润度。", - "feature.skin.enable_advanced_skin": "启用高级皮肤", - "feature.skin.enable_skin_detail": "启用皮肤细节", - "feature.skin.enable_skin_detail_texture": "启用皮肤细节纹理", - "feature.skin.enable_sss_transmission": "启用 SSS 透射", - "feature.skin.extra_edge_roughness": "额外边缘粗糙度", - "feature.skin.extra_roughness_at_the_edges_of_the_skin": "皮肤边缘的额外粗糙度,用于近似脸部细绒毛。", - "feature.skin.extra_skin_wetness": "额外皮肤湿润度", - "feature.skin.fresnel_f0": "菲涅尔 F0", - "feature.skin.fresnel_reflectance": "菲涅尔反射率", - "feature.skin.full_sweat_threshold": "满汗阈值", - "feature.skin.fuzz_f0": "细绒毛 F0", - "feature.skin.fuzz_roughness": "细绒毛粗糙度", - "feature.skin.fuzz_strength": "细绒毛强度", - "feature.skin.how_many_seconds_it_takes_for_skin_to": "离开水后皮肤完全变干所需的秒数。较高的值会让湿润持续更久。", - "feature.skin.intensity_of_secondary_specular_highlights": "次级镜面反射高光强度。", - "feature.skin.key_feature_1": "基于物理的双镜面反射叶瓣,提供更真实的皮肤高光", - "feature.skin.key_feature_2": "平铺皮肤细节纹理,提升真实感", - "feature.skin.key_feature_3": "支持额外的粗糙度、半透明和湿润度纹理", - "feature.skin.key_feature_4": "重做湿润系统,用于动态皮肤效果", - "feature.skin.multiplier_for_specular_map": "镜面反射贴图倍率", - "feature.skin.multiplier_for_the_base_color_texture": "基础色纹理倍率。", - "feature.skin.multiply_the_tiling_for_the_body_to_match": "将身体平铺倍率乘上该值以匹配脸部。", - "feature.skin.name": "高级皮肤", - "feature.skin.options_for_additional_roughness_and_specular_maps": "额外粗糙度和镜面反射贴图选项。", - "feature.skin.physical_main_roughness_multiplier": "物理主粗糙度倍率", - "feature.skin.physical_second_roughness_multiplier": "物理次粗糙度倍率", - "feature.skin.physical_specular_multiplier": "物理镜面反射倍率", - "feature.skin.primary_roughness": "主粗糙度", - "feature.skin.reload_skin_detail_texture": "重新加载皮肤细节纹理", - "feature.skin.secondary_roughness": "次粗糙度", - "feature.skin.secondary_specular_strength": "次级镜面反射强度", - "feature.skin.should_be_30_50_lower_than_primary": "应比主粗糙度低 30-50%%。", - "feature.skin.skin_detail_strength": "皮肤细节强度", - "feature.skin.skin_detail_tiling": "皮肤细节平铺", - "feature.skin.smoothness_of_epidermal_cell_layer_reflections": "表皮细胞层反射的平滑度。", - "feature.skin.specular_texture_multiplier": "镜面反射纹理倍率", - "feature.skin.sss_width": "SSS 宽度", - "feature.skin.stamina_threshold_for_sweat": "出汗耐力阈值", - "feature.skin.strength_of_skin_detail_texture": "皮肤细节纹理强度。", - "feature.skin.the_character_reaches_maximum_sweat_when_stamina_drops": "当耐力低于此百分比时,角色达到最大出汗量。例如 0.15 表示耐力低于 15%% 时满汗。", - "feature.skin.the_character_starts_sweating_when_their_stamina_drops": "当耐力低于此百分比时,角色开始出汗。例如 0.75 表示耐力低于 75%% 时出现汗水。", - "feature.skin.the_more_tiling_the_more_detailed_the_skin": "平铺越多,皮肤细节越丰富。", - "feature.skin.translucency": "半透明度", - "feature.skin.translucency_of_the_sss_transmittance_effect": "SSS 透射效果的半透明度。", - "feature.skin.use_dynamic_wetness": "使用动态湿润度", - "feature.skin.wetness_fade_out_time": "湿润度淡出时间", - "feature.skin.wetness_normal_scale": "湿润法线强度", - "feature.skin.wetness_perlin_noise_lacunarity": "湿润 Perlin 噪声频率倍增", - "feature.skin.wetness_perlin_noise_persistence": "湿润 Perlin 噪声持续度", - "feature.skin.wetness_perlin_noise_scale": "湿润 Perlin 噪声比例", - "feature.skin.width_of_the_sss_transmittance_effect": "SSS 透射效果的宽度。", "cs_editor.actions": "操作", "cs_editor.active": "活跃:", "cs_editor.active_click_pause": "活跃 - 点击暂停", @@ -1648,6 +50,7 @@ "cs_editor.click_to_copy": "点击复制", "cs_editor.clip_distance": "裁剪距离", "cs_editor.close_all_widgets": "关闭所有 {} 控件", + "cs_editor.close_cs_editor": "关闭 CS 编辑器(Esc)", "cs_editor.cloud_alpha": "云透明度", "cs_editor.cloud_color": "云颜色", "cs_editor.cloud_layer": "云层 {}", @@ -1676,6 +79,7 @@ "cs_editor.confirm_delete_saved_file": "您确定要删除已保存的设置文件吗?", "cs_editor.contribution": "贡献度", "cs_editor.copy_all_from_parent": "从父级天气复制所有参数值", + "cs_editor.cs_editor": "CS 编辑器", "cs_editor.currently_exterior_cell": "您当前处于室外单元格。", "cs_editor.custom_color": "自定义颜色", "cs_editor.custom_color_contribution": "自定义颜色贡献度", @@ -1857,6 +261,7 @@ "cs_editor.palette": "调色板", "cs_editor.parameter": "参数", "cs_editor.parent": "父级", + "cs_editor.parent_cs_editor_feature": "仅编辑器功能:设置父级天气以从中复制设置。", "cs_editor.particle_density_label": "粒子密度", "cs_editor.particle_shader": "粒子着色器", "cs_editor.particle_size": "粒子大小", @@ -2009,6 +414,26 @@ "cs_editor.xy_rotation": "XY 旋转", "cs_editor.yes_delete": "是,删除", "cs_editor.z_rotation": "Z 旋转", + "feature.category.characters": "角色", + "feature.category.display": "显示", + "feature.category.grass": "草地", + "feature.category.landscape_and_textures": "地形与纹理", + "feature.category.lighting": "光照", + "feature.category.materials": "材质", + "feature.category.other": "其他", + "feature.category.post_processing": "后处理", + "feature.category.sky": "天空", + "feature.category.utility": "工具", + "feature.category.water": "水面", + "feature.cloud_shadows.description": "为地形和物体投射逼真的云阴影,当云层掠过时产生动态的光影变化,增强氛围沉浸感。", + "feature.cloud_shadows.key_feature_1": "地形与物体的动态云阴影投射", + "feature.cloud_shadows.key_feature_2": "可配置的阴影不透明度,便于艺术控制", + "feature.cloud_shadows.key_feature_3": "与云层运动同步的实时阴影移动", + "feature.cloud_shadows.key_feature_4": "基于立方体贴图的精确阴影投射计算", + "feature.cloud_shadows.key_feature_5": "增强的天空渲染集成", + "feature.cloud_shadows.name": "云阴影", + "feature.cloud_shadows.opacity": "不透明度", + "feature.cloud_shadows.opacity_tooltip": "值越高,云阴影越暗。", "feature.cs_editor.accelerate_weather_change": "加速天气变化", "feature.cs_editor.accelerate_weather_change_tooltip": "启用时,天气变化即时生效", "feature.cs_editor.aurora": "极光", @@ -2018,6 +443,7 @@ "feature.cs_editor.collapse": "折叠", "feature.cs_editor.current_weather": "当前天气:%s", "feature.cs_editor.current_weather_column": "当前天气", + "feature.cs_editor.description": "用于在游戏内检查、编辑和预览面向渲染器数据的开发工具。", "feature.cs_editor.effective_wind_dir": "有效风向:%.1f°(原始 - %.1f°)", "feature.cs_editor.expand": "展开", "feature.cs_editor.feature_weather_analysis_tooltip_0": "天气分析提供方:", @@ -2026,6 +452,14 @@ "feature.cs_editor.filter_by_weather_type": "按天气类型筛选:", "feature.cs_editor.has_custom_settings": "有自定义设置", "feature.cs_editor.headwind": "逆风(风朝向玩家)", + "feature.cs_editor.key_feature_1": "提供天气编辑功能", + "feature.cs_editor.key_feature_2": "包含对原版后处理和天气设置的动态保存与加载。", + "feature.cs_editor.key_feature_3": "实时编辑和预览效果", + "feature.cs_editor.key_feature_4": "即时切换任意天气,支持立即或渐变过渡", + "feature.cs_editor.key_feature_5": "按类型筛选天气(晴朗、多云、雨天、雪天、极光),方便浏览", + "feature.cs_editor.key_feature_6": "查看详细的天气信息,包括风、降水和闪电数据", + "feature.cs_editor.key_feature_7": "颜色编码的天气名称,一目了然地展示所有天气属性", + "feature.cs_editor.key_feature_8": "持久叠加窗口,可在游戏过程中持续监控天气", "feature.cs_editor.last_weather_column": "上次天气", "feature.cs_editor.left_crosswind": "左侧横风", "feature.cs_editor.lightning_begin_fade_in": "闪电开始淡入:%.3f(原始%u)", @@ -2036,6 +470,7 @@ "feature.cs_editor.lightning_fade_info_2": "结束淡出:闪电完全消失的点", "feature.cs_editor.lightning_fade_info_3": "原始值:0-255(uint8),归一化:0.0-1.0", "feature.cs_editor.lock_weather": "锁定天气", + "feature.cs_editor.name": "CS 编辑器", "feature.cs_editor.no_active_weather": "无活跃天气", "feature.cs_editor.no_precipitation_data": "粒子密度:无降水数据", "feature.cs_editor.no_transition": "过渡来源:无过渡", @@ -2110,7 +545,1572 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = 顺风(风在玩家背后)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = 横风(左/右)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = 逆风(风朝向玩家)", - "cs_editor.close_cs_editor": "关闭 CS 编辑器(Esc)", - "cs_editor.cs_editor": "CS 编辑器", - "cs_editor.parent_cs_editor_feature": "仅编辑器功能:设置父级天气以从中复制设置。" + "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", + "feature.dynamic_cubemaps.color": "颜色", + "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", + "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", + "feature.dynamic_cubemaps.dynamic_cubemap_creator": "动态立方体贴图创建器", + "feature.dynamic_cubemaps.enable_creator": "启用创建器", + "feature.dynamic_cubemaps.enable_ssr": "启用屏幕空间反射", + "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", + "feature.dynamic_cubemaps.export": "导出", + "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", + "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", + "feature.dynamic_cubemaps.key_feature_3": "增强的水面反射,包含环境细节", + "feature.dynamic_cubemaps.key_feature_4": "支持标准和VR两种渲染模式", + "feature.dynamic_cubemaps.key_feature_5": "优化的立方体贴图推理与辐照度计算", + "feature.dynamic_cubemaps.name": "动态立方体贴图", + "feature.dynamic_cubemaps.roughness": "粗糙度", + "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", + "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", + "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", + "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", + "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", + "feature.exp_height_fog.debug": "调试", + "feature.exp_height_fog.depth_distribution_scale": "深度分布比例", + "feature.exp_height_fog.dir_inscattering_anisotropy": "方向光内散射各向异性", + "feature.exp_height_fog.dir_inscattering_anisotropy_tooltip": "通过Henyey-Greenstein相位函数控制内散射的不对称性。\n正值产生前向散射(太阳周围发光)。\n零为各向同性。负值产生后向散射。", + "feature.exp_height_fog.dir_inscattering_mul": "方向光内散射倍率", + "feature.exp_height_fog.directional_scattering_intensity": "方向光散射强度", + "feature.exp_height_fog.directional_shadow_bias": "方向阴影偏移", + "feature.exp_height_fog.disable_vanilla_fog": "禁用原版雾", + "feature.exp_height_fog.disable_vanilla_fog_tooltip": "完全禁用原版雾。仅应用指数高度雾。", + "feature.exp_height_fog.enable_exp_height_fog": "启用指数高度雾", + "feature.exp_height_fog.enable_volumetric_fog": "启用体积雾", + "feature.exp_height_fog.fog_density": "雾密度", + "feature.exp_height_fog.fog_height": "雾高度", + "feature.exp_height_fog.fog_height_falloff": "雾高度衰减", + "feature.exp_height_fog.fog_inscattering_color": "雾内散射颜色", + "feature.exp_height_fog.grid_depth_slices": "网格深度切片", + "feature.exp_height_fog.grid_pixel_size": "网格像素大小", + "feature.exp_height_fog.history_miss_samples": "历史缺失采样数", + "feature.exp_height_fog.inscattering_cubemap_tint": "内散射立方体贴图色调", + "feature.exp_height_fog.local_light_scattering_intensity": "局部光散射强度", + "feature.exp_height_fog.near_fade_in_distance": "近处淡入距离", + "feature.exp_height_fog.original_fog_color_amount": "原始雾颜色量", + "feature.exp_height_fog.sample_jitter_multiplier": "采样抖动倍率", + "feature.exp_height_fog.sample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.LightScatteringSampleJitterMultiplier。\n在 Halton 序列基础上为每个体素添加随机偏移。\n0 = UE 默认值;非零值需要更强的时域滤波。", + "feature.exp_height_fog.sky_lighting_scattering_intensity": "天空光照散射强度", + "feature.exp_height_fog.start_distance": "起始距离", + "feature.exp_height_fog.sunlight_attenuation": "阳光衰减量", + "feature.exp_height_fog.temporal_history_weight": "时域历史权重", + "feature.exp_height_fog.upsample_jitter_multiplier": "上采样抖动倍率", + "feature.exp_height_fog.upsample_jitter_multiplier_tooltip": "对应 UE 的 r.VolumetricFog.UpsampleJitterMultiplier。\n在屏幕空间抖动最终 3D 雾查找,以隐藏\n低分辨率 froxel 像素化。0 = UE 默认值。", + "feature.exp_height_fog.use_dynamic_cubemaps": "使用动态立方体贴图进行内散射", + "feature.exp_height_fog.volumetric_albedo": "体积雾反照率", + "feature.exp_height_fog.volumetric_emissive": "体积雾自发光", + "feature.exp_height_fog.volumetric_extinction_scale": "体积雾消光比例", + "feature.exp_height_fog.volumetric_fog": "体积雾", + "feature.exp_height_fog.volumetric_scattering_distribution": "体积雾散射分布", + "feature.exp_height_fog.volumetric_start_distance": "体积雾起始距离", + "feature.exp_height_fog.volumetric_view_distance": "体积雾视距", + "feature.exponential_height_fog.description": "添加逼真的高度雾效果,雾密度随高度变化,增强场景的大气深度和沉浸感。", + "feature.exponential_height_fog.key_feature_1": "新增指数高度雾效果", + "feature.exponential_height_fog.key_feature_2": "适配原版雾效设置", + "feature.exponential_height_fog.key_feature_3": "营造大气深度感", + "feature.exponential_height_fog.name": "指数高度雾", + "feature.extended_materials.complex_material": "复杂材质", + "feature.extended_materials.description": "扩展材质添加了包括视差遮蔽映射和复杂材质混合在内的高级材质效果。\n此功能可增强表面细节和深度感知,呈现更逼真的纹理。", + "feature.extended_materials.enable_complex_material": "启用复杂材质", + "feature.extended_materials.enable_complex_material_tooltip": "启用利用环境遮罩的复杂材质规范支持。包括视差贴图,以及更逼真的金属和镜面反射。对于环境遮罩中alpha通道无效的模组内容,可能导致纹理变形。", + "feature.extended_materials.enable_height_blending": "启用地形高度混合", + "feature.extended_materials.enable_height_blending_tooltip": "基于视差启用地形纹理混合。", + "feature.extended_materials.enable_legacy_terrain": "启用旧版地形", + "feature.extended_materials.enable_legacy_terrain_tooltip": "使用每张地形纹理的alpha通道启用地形视差。因此,所有地形纹理必须支持视差才能使效果正常工作。", + "feature.extended_materials.enable_parallax": "启用视差", + "feature.extended_materials.enable_parallax_tooltip": "在为视差制作的标准网格上启用视差效果。", + "feature.extended_materials.enable_parallax_warping_fix": "启用视差变形修复", + "feature.extended_materials.enable_parallax_warping_fix_tooltip": "启用修复,减少弯曲和平滑法线三角形上的视差缩放。", + "feature.extended_materials.enable_shadows": "启用阴影", + "feature.extended_materials.enable_shadows_tooltip": "使用视差时启用廉价软阴影。适用于所有方向光和点光源。", + "feature.extended_materials.extend_shadows": "扩展阴影", + "feature.extended_materials.extend_shadows_tooltip": "将视差阴影扩展到视差范围之外。对性能影响较小。", + "feature.extended_materials.key_feature_1": "视差遮蔽映射,增加深度感", + "feature.extended_materials.key_feature_2": "复杂材质混合", + "feature.extended_materials.key_feature_3": "地形高度图支持", + "feature.extended_materials.key_feature_4": "视差阴影", + "feature.extended_materials.key_feature_5": "基于高度的纹理混合", + "feature.extended_materials.name": "扩展材质", + "feature.extended_materials.parallax": "视差", + "feature.extended_materials.soft_shadows": "近似软阴影", + "feature.extended_translucency.alpha_mode_anisotropic_fabric": "3 - 各向异性织物", + "feature.extended_translucency.alpha_mode_disabled": "0 - 禁用", + "feature.extended_translucency.alpha_mode_isotropic_fabric": "2 - 各向同性织物、玻璃等", + "feature.extended_translucency.alpha_mode_rim_edge": "1 - 边缘光", + "feature.extended_translucency.blend_weight": "混合权重", + "feature.extended_translucency.blend_weight_tooltip": "控制效果应用于最终结果的混合权重。", + "feature.extended_translucency.default_material_model": "默认材质模型", + "feature.extended_translucency.default_material_model_tooltip": "各向异性半透明将根据您查看半透明表面的视角调整不透明度。\n - 禁用:无各向异性半透明,平坦Alpha。\n - 边缘光:无物理模型的简单边缘光效果,几何体边缘始终不透明,即使完全透明。\n - 各向同性织物:由单一方向编织的虚构织物,尊重法线贴图,也适用于玻璃面板层。\n - 各向异性织物:由切线和副法线方向编织的常见织物,忽略法线贴图。\n", + "feature.extended_translucency.description": "为薄织物和其他半透明材质提供逼真的渲染效果。\n支持多种材质模型,适用于不同类型的半透明表面。", + "feature.extended_translucency.key_feature_1": "多种半透明材质模型(边缘光、各向同性/各向异性织物)", + "feature.extended_translucency.key_feature_2": "逼真的织物半透明效果,支持方向光透射", + "feature.extended_translucency.key_feature_3": "通过NIF额外数据支持逐材质覆写", + "feature.extended_translucency.key_feature_4": "可配置的透明度与柔和度控制", + "feature.extended_translucency.key_feature_5": "性能优化的半透明计算", + "feature.extended_translucency.name": "扩展半透明", + "feature.extended_translucency.skinned_mesh_only": "仅蒙皮网格", + "feature.extended_translucency.skinned_mesh_only_tooltip": "控制此效果是否仅应用于蒙皮网格。如果在随机对象上看到不期望的效果,请勾选此选项。", + "feature.extended_translucency.softness": "柔和度", + "feature.extended_translucency.softness_tooltip": "控制Alpha增加的柔和度,增加柔和度会减少Alpha的增加量。", + "feature.extended_translucency.translucent_material": "半透明材质", + "feature.extended_translucency.transparency_increase": "透明度增加", + "feature.extended_translucency.transparency_increase_tooltip": "半透明材质会使材质平均更不透明,这可能与预期不同。降低Alpha以抵消此效果并增加输出的动态范围。", + "feature.grass_collision.description": "启用动态草地交互——当角色走过草地时,草会弯曲和摆动,营造更沉浸的环境反应。", + "feature.grass_collision.enable": "启用草地碰撞", + "feature.grass_collision.grass_collision": "草地碰撞", + "feature.grass_collision.key_feature_1": "角色移动带动实时草地变形", + "feature.grass_collision.key_feature_2": "最多支持256个同时交互的碰撞检测", + "feature.grass_collision.key_feature_3": "动态追踪角色位置以驱动草地响应", + "feature.grass_collision.key_feature_4": "性能优化的碰撞计算", + "feature.grass_collision.key_feature_5": "与现有草地渲染无缝集成", + "feature.grass_collision.name": "草地碰撞", + "feature.grass_lighting.basic_grass": "基础草地", + "feature.grass_lighting.brightness": "亮度", + "feature.grass_lighting.brightness_tooltip": "将草地纹理变暗,以便在新光照下看起来更好", + "feature.grass_lighting.complex_grass": "复杂草地", + "feature.grass_lighting.description": "通过改进的光照、高光和次表面散射,增强草地渲染效果。\n使草地看起来更自然,对光照条件反应更灵敏。", + "feature.grass_lighting.detection_header": "复杂草地检测", + "feature.grass_lighting.detection_threshold": "检测阈值", + "feature.grass_lighting.detection_threshold_tooltip": "检测复杂草地纹理的阈值。值越低越严格。", + "feature.grass_lighting.effects": "效果", + "feature.grass_lighting.glossiness": "光泽度", + "feature.grass_lighting.glossiness_tooltip": "高光光泽度。", + "feature.grass_lighting.key_feature_1": "增强的草地光照模型", + "feature.grass_lighting.key_feature_2": "草地上的高光反射", + "feature.grass_lighting.key_feature_3": "次表面散射效果", + "feature.grass_lighting.key_feature_4": "提升草地视觉质量", + "feature.grass_lighting.key_feature_5": "可配置的材质属性", + "feature.grass_lighting.lighting": "光照", + "feature.grass_lighting.name": "草地光照", + "feature.grass_lighting.override_complex": "覆盖复杂草地光照设置", + "feature.grass_lighting.override_complex_tooltip": "覆盖草地网格作者设置的参数。复杂草地作者可以为其草地网格定义亮度。然而,某些作者可能未考虑Community Shaders提供的额外光源。此选项将其草地设置视为非复杂草地。这是Community Shaders < 0.7.0中的默认行为", + "feature.grass_lighting.specular_desc": "复杂草地的高光", + "feature.grass_lighting.specular_strength": "高光强度", + "feature.grass_lighting.specular_strength_tooltip": "高光强度。", + "feature.grass_lighting.sss_amount": "SSS量", + "feature.grass_lighting.sss_tooltip": "次表面散射(SSS)量。柔和光照控制物体的均匀照明程度。背光照明照亮物体的背面。两者结合模拟光线穿过表面的传输。", + "feature.hair_specular.description": "提供更好的头发着色效果,具有逼真的高光反射和基于切线的光线交互,呈现更生动的头发外观。", + "feature.hair_specular.diffuse_multiplier": "漫反射倍率", + "feature.hair_specular.enable_self_shadow": "启用屏幕空间自阴影", + "feature.hair_specular.enable_self_shadow_tooltip": "为头发启用屏幕空间自阴影。\nMarschner头发模型在没有自阴影的情况下可能会有过亮的透射。\n", + "feature.hair_specular.enable_tangent_shift": "启用切线偏移", + "feature.hair_specular.enable_tangent_shift_tooltip": "启用使用切线偏移纹理来改变发丝上的高光变化。\n结果可能因使用的头发模型而异。\n", + "feature.hair_specular.enabled": "启用", + "feature.hair_specular.glossiness": "光泽度", + "feature.hair_specular.glossiness_tooltip": "控制头发的光泽度。\nKajiya-Kay模式中光泽度映射到高光指数。\nMarschner模式中控制头发表面的粗糙度。\n", + "feature.hair_specular.hair_base_color_multiplier": "头发基色倍率", + "feature.hair_specular.hair_mode": "头发模式", + "feature.hair_specular.hair_mode_tooltip": "选择要使用的头发着色模型。\nKajiya-Kay是模拟头发高光的经验模型。\nMarschner是更基于物理的模型,模拟头发光交互。\n两种模型都是各向异性的,支持基于切线的着色。\n没有自阴影时,Marschner可能因透射而显得过亮。\n", + "feature.hair_specular.hair_saturation": "头发饱和度", + "feature.hair_specular.indirect_diffuse_multiplier": "间接漫反射倍率", + "feature.hair_specular.indirect_specular_multiplier": "间接高光倍率", + "feature.hair_specular.key_feature_1": "逼真的头发高光反射", + "feature.hair_specular.key_feature_2": "增强的头发光泽度与饱和度控制", + "feature.hair_specular.key_feature_3": "独立的高光和漫反射光照倍率", + "feature.hair_specular.key_feature_4": "切线偏移纹理支持,实现多样的头发高光效果", + "feature.hair_specular.name": "头发高光", + "feature.hair_specular.primary_tangent_shift": "主高光切线偏移", + "feature.hair_specular.secondary_tangent_shift": "次高光切线偏移", + "feature.hair_specular.self_shadow_exponent": "自阴影指数", + "feature.hair_specular.self_shadow_scale": "自阴影缩放", + "feature.hair_specular.self_shadow_strength": "自阴影强度", + "feature.hair_specular.specular_multiplier": "高光倍率", + "feature.hair_specular.transmission": "透射", + "feature.hdr_display.advanced": "高级", + "feature.hdr_display.advanced_tooltip_enable_windows_hdr": "建议启用 Windows HDR,而不是在这里强制开启。", + "feature.hdr_display.advanced_tooltip_force_enable": "即使未检测到,也强制启用 HDR(不推荐)。", + "feature.hdr_display.cancel": "取消", + "feature.hdr_display.capable_display_windows_hdr_off": "支持 HDR 的显示器(Windows HDR 已关闭)", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_0": "你的显示器支持 HDR,但 Windows HDR 当前已关闭。", + "feature.hdr_display.capable_display_windows_hdr_off_tooltip_1": "请在 Windows 显示设置中启用 HDR,以允许自动检测。", + "feature.hdr_display.description": "为HDR显示器提供真正的高动态范围输出。", + "feature.hdr_display.display_detected": "检测到 HDR 显示器", + "feature.hdr_display.display_reports_max_nits": "显示器报告的最大亮度:%.0f 尼特", + "feature.hdr_display.display_reports_max_nits_tooltip_0": "该值由操作系统或驱动(DXGI MaxLuminance)报告,并非直接测量值。", + "feature.hdr_display.display_reports_max_nits_tooltip_1": "它可能来自 EDID 元数据,因此可能与真实高光峰值亮度不同。", + "feature.hdr_display.display_reports_max_nits_tooltip_2": "请把它当作初始参考值,并按需调节峰值亮度。", + "feature.hdr_display.dont_show_again": "不再显示此提示", + "feature.hdr_display.enable_hdr": "启用 HDR", + "feature.hdr_display.enable_hdr_tooltip": "启用 HDR 输出。在扩展动态范围下尽量保持与原版相近的视觉效果。", + "feature.hdr_display.enable_hdr_tooltip_not_detected": "未检测到 HDR 显示器。可使用“高级”按钮强制开启。", + "feature.hdr_display.enable_hdr_tooltip_windows_off": "显示器支持 HDR,但 Windows HDR 已关闭。请先在 Windows 显示设置中启用 HDR,然后重启游戏。", + "feature.hdr_display.enabled_without_detected_display": "HDR 已启用,但未检测到 HDR 显示器。", + "feature.hdr_display.exclusive_fullscreen_warning": "警告:检测到独占全屏模式。", + "feature.hdr_display.exclusive_fullscreen_warning_detail": "HDR 与独占全屏不兼容,可能无法正常工作。请切换到无边框窗口模式以获得正确的 HDR 支持。", + "feature.hdr_display.force_enable_hdr": "强制启用 HDR", + "feature.hdr_display.force_enable_hdr_confirm": "仅当你确实拥有 HDR 显示器,但它未被正确检测到时,才应继续。", + "feature.hdr_display.force_enable_hdr_detected_warning": "未在你的显示器上检测到 HDR。", + "feature.hdr_display.force_enable_hdr_sdr_warning": "如果你使用的是 SDR(标准动态范围)显示器,游戏画面会非常不正常。", + "feature.hdr_display.force_enable_hdr_warning": "警告:强制启用 HDR", + "feature.hdr_display.key_feature_1": "支持HDR10输出(10位色深),升级HDR缓冲区至16位,完全无裁剪的渲染管线以实现真正的HDR数值。", + "feature.hdr_display.key_feature_2": "基于Skyrim ISHDR路径的HDR感知色调映射(Reinhard/Hejl-Burgess-Dawson),在保留原版风格的同时改善HDR显示器上的高光处理。", + "feature.hdr_display.key_feature_3": "可配置的纸张白点和峰值亮度。", + "feature.hdr_display.name": "HDR 显示", + "feature.hdr_display.paper_white_nits": "纸白亮度(尼特)", + "feature.hdr_display.paper_white_tooltip_0": "控制 SDR 白色在 HDR 显示器上的显示亮度。", + "feature.hdr_display.paper_white_tooltip_1": "203 尼特是 ITU BT.2408 参考值。提高该值可获得更亮的画面。", + "feature.hdr_display.peak_brightness_nits": "峰值亮度(尼特)", + "feature.hdr_display.peak_brightness_tooltip_0": "显示器可输出的最大亮度。", + "feature.hdr_display.peak_brightness_tooltip_1": "请设置为与你显示器真实峰值亮度相匹配的数值。", + "feature.hdr_display.sdr_display_not_detected": "SDR 显示器(未检测到 HDR)", + "feature.hdr_display.ui_brightness_multiplier": "UI 亮度倍率", + "feature.hdr_display.ui_brightness_multiplier_tooltip_0": "在 HDR 模式下,UI 亮度 = 纸白亮度 x 此倍率。", + "feature.hdr_display.ui_brightness_multiplier_tooltip_1": "1.00x 表示 UI 以纸白亮度渲染。更高的值会让 UI 相对场景内容更亮。", + "feature.hdr_display.ui_brightness_multiplier_tooltip_2": "注意:主菜单和加载画面始终以纸白亮度渲染。", + "feature.hdr_display.warning_popup_title": "HDR 警告", + "feature.ibl.dalc_amount": "DALC量", + "feature.ibl.dalc_amount_tooltip": "将IBL亮度向游戏原版环境光(DALC)级别混合。\n0 = 不匹配(纯IBL亮度),1 = 完全匹配原版环境光。", + "feature.ibl.dalc_mode": "DALC模式", + "feature.ibl.dalc_mode_color_ratio": "颜色比例", + "feature.ibl.dalc_mode_dalc_plus_sky": "DALC + 天空", + "feature.ibl.dalc_mode_dalc_plus_sky_directional": "DALC + 天空(定向)", + "feature.ibl.dalc_mode_luminance_ratio": "亮度比例", + "feature.ibl.dalc_mode_tooltip": "DALC与IBL亮度比率的计算方式:\n亮度比:来自总亮度的标量比率(丢失DALC颜色色调)。\n颜色比:逐通道比率(保留DALC颜色色调)。\nDALC + 天空:使用原版环境光作为基础,天空IBL叠加。天光仅影响天空。\nDALC + 天空(方向性):相同,但天光也按方向降低原版环境光。", + "feature.ibl.description": "用基于物理的IBL替代游戏的环境光照,IBL从立方体贴图的球谐函数中推导得出。", + "feature.ibl.disable_in_interiors": "在室内禁用", + "feature.ibl.disable_in_interiors_tooltip": "在室内单元中禁用IBL。", + "feature.ibl.enable_ibl": "启用IBL", + "feature.ibl.enable_ibl_tooltip": "切换IBL。启用时,环境光来自立方体贴图球谐函数,而非原版系统。", + "feature.ibl.env_ibl_saturation": "环境IBL饱和度", + "feature.ibl.env_ibl_saturation_tooltip": "环境IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", + "feature.ibl.env_ibl_scale": "环境IBL缩放", + "feature.ibl.env_ibl_scale_tooltip": "环境IBL的强度倍率(来自动态立方体贴图)。\n控制周围环境对环境光照的贡献强度。", + "feature.ibl.fog_mix": "雾混合", + "feature.ibl.fog_mix_tooltip": "将雾颜色向IBL环境光颜色混合。\n0 = 原版雾,1 = 雾完全由IBL着色。", + "feature.ibl.key_feature_1": "将环境和天空立方体贴图投影为球谐函数(SH)以计算辐照度", + "feature.ibl.key_feature_2": "双IBL源:环境立方体贴图(动态立方体贴图)和Skyrim原生天空反射立方体贴图", + "feature.ibl.key_feature_3": "DALC亮度匹配,保持IBL与游戏环境光水平一致", + "feature.ibl.key_feature_4": "可配置的每源强度、饱和度、雾混合以及每天气覆写", + "feature.ibl.key_feature_5": "静态IBL回退纹理,用于世界外对象(如物品栏物品)", + "feature.ibl.name": "基于图像的光照", + "feature.ibl.preserve_fog_luminance": "保持雾亮度", + "feature.ibl.preserve_fog_luminance_tooltip": "当雾混合激活时,重新缩放IBL着色的雾以保持原始雾亮度。\n防止雾变得过亮或过暗。", + "feature.ibl.sky_ibl_saturation": "天空IBL饱和度", + "feature.ibl.sky_ibl_saturation_tooltip": "天空IBL的颜色饱和度。\n较低值产生更中性的环境光;较高值产生更鲜艳的颜色。", + "feature.ibl.sky_ibl_scale": "天空IBL缩放", + "feature.ibl.sky_ibl_scale_tooltip": "天空IBL的强度倍率(来自游戏的原始反射立方体贴图)。\n控制天空对环境光照的贡献强度。", + "feature.ibl.use_static_ibl": "对世界外物体使用静态IBL", + "feature.ibl.use_static_ibl_tooltip": "对在游戏世界外渲染的物体(如物品栏物品、加载画面)使用预烘焙的静态IBL立方体贴图纹理。", + "feature.interior_sun.description": "允许太阳和月亮的光线和阴影照射到室内空间。", + "feature.interior_sun.force_double_sided": "强制双面渲染", + "feature.interior_sun.force_double_sided_tooltip": "在室内太阳阴影贴图渲染期间禁用背面剔除。将防止大部分通过未遮罩/未准备好的室内的漏光,性能成本较小。", + "feature.interior_sun.interior_shadow_distance": "室内阴影距离", + "feature.interior_sun.interior_shadow_distance_tooltip": "设置在室内渲染阴影的距离。较低值提供更高质量的阴影并改善性能,但可能导致远处室内空间照亮不正确。", + "feature.interior_sun.key_feature_1": "仅对明确启用的室内空间生效", + "feature.interior_sun.key_feature_2": "利用现有的太阳、月亮和天气系统", + "feature.interior_sun.key_feature_3": "包含强制双面渲染选项,适用于未准备的室内场景", + "feature.interior_sun.key_feature_4": "修复导致漏光的几何体裁剪问题", + "feature.interior_sun.name": "室内阳光", + "feature.inverse_square_lighting.description": "为光照实现额外的平方反比衰减,使光照衰减更加物理准确和逼真。", + "feature.inverse_square_lighting.key_feature_1": "基于强度自动计算光照半径", + "feature.inverse_square_lighting.key_feature_2": "光源在可配置的截止距离处平滑淡出,解决无限距离问题", + "feature.inverse_square_lighting.key_feature_3": "不修改任何现有光照", + "feature.inverse_square_lighting.key_feature_4": "需要使用开启了平方反比衰减的模组光源。", + "feature.inverse_square_lighting.key_feature_5": "与Light Placer完全集成", + "feature.inverse_square_lighting.name": "平方反比光照", + "feature.key_features": "主要特性:", + "feature.light_editor.active_shadow_lights": "活跃阴影光源:%u", + "feature.light_editor.base_object": "基础对象:0x%08X | %s", + "feature.light_editor.cell": "单元格:%s", + "feature.light_editor.color": "颜色", + "feature.light_editor.cutoff": "截止", + "feature.light_editor.disable_inverse_square_falloff_lights": "禁用平方反比衰减光源", + "feature.light_editor.disable_regular_falloff_lights": "禁用常规衰减光源", + "feature.light_editor.dynamic": "动态", + "feature.light_editor.filter_by": "过滤方式", + "feature.light_editor.flicker": "闪烁", + "feature.light_editor.flicker_slow": "缓慢闪烁", + "feature.light_editor.hemi_shadow": "半球阴影", + "feature.light_editor.intensity": "强度", + "feature.light_editor.inverse_square_light": "平方反比光源", + "feature.light_editor.ligh": "LIGH:0x%08X | %s", + "feature.light_editor.light_flags": "光源标志", + "feature.light_editor.lights": "光源", + "feature.light_editor.linear_light": "线性光源", + "feature.light_editor.memory_address": "内存地址:%p", + "feature.light_editor.negative": "负向", + "feature.light_editor.ni_light_name": "NiLight名称:%s", + "feature.light_editor.omni_shadow": "全向阴影", + "feature.light_editor.owner": "所有者:0x%08X | %s", + "feature.light_editor.owner_last_edited_by": "所有者最后编辑者:%s", + "feature.light_editor.portal_strict": "传送门严格", + "feature.light_editor.position_format": "X:%.2f,Y:%.2f,Z:%.2f", + "feature.light_editor.position_offset": "位置偏移", + "feature.light_editor.pulse": "脉冲", + "feature.light_editor.pulse_slow": "缓慢脉冲", + "feature.light_editor.radius": "半径", + "feature.light_editor.revert_changes": "还原更改", + "feature.light_editor.save_to_light_placer": "保存到Light Placer", + "feature.light_editor.save_to_light_placer_tooltip": "将当前设置保存到Light Placer JSON。", + "feature.light_editor.select_a_light": "选择光源", + "feature.light_editor.shadows_only": "仅阴影", + "feature.light_editor.shadows_only_tooltip": "仅显示带有HemiShadow或OmniShadow标志的光源。", + "feature.light_editor.size": "大小", + "feature.light_editor.sort_by": "排序方式", + "feature.light_editor.spotlight_not_applicable": "聚光灯:ISL光源类型标志不适用", + "feature.light_editor.total_lights": "总光源数:%u", + "feature.light_limit_fix.debug": "调试", + "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", + "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", + "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", + "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", + "feature.light_limit_fix.key_feature_1": "移除4光源限制", + "feature.light_limit_fix.key_feature_2": "无限动态光源", + "feature.light_limit_fix.key_feature_3": "提升光照质量", + "feature.light_limit_fix.key_feature_4": "增强视觉真实感", + "feature.light_limit_fix.key_feature_5": "增强视觉真实感", + "feature.light_limit_fix.light_limit_vis": "光源限制可视化", + "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", + "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", + "feature.light_limit_fix.name": "光源限制修复", + "feature.light_limit_fix.statistics": "统计", + "feature.linear_lighting.ambient_gamma": "环境伽马", + "feature.linear_lighting.ambient_multiplier": "环境倍率", + "feature.linear_lighting.blood_effects_multiplier": "血液效果倍率", + "feature.linear_lighting.color_gamma": "颜色伽马", + "feature.linear_lighting.deferred_effects_multiplier": "延迟效果倍率", + "feature.linear_lighting.description": "通过色彩空间转换来提高光照计算的准确性。", + "feature.linear_lighting.directional_light_multiplier": "方向光倍率", + "feature.linear_lighting.effect_gamma": "效果伽马", + "feature.linear_lighting.effect_lighting_multiplier": "效果光照倍率", + "feature.linear_lighting.effect_transparency_gamma": "效果透明度伽马", + "feature.linear_lighting.effects": "效果", + "feature.linear_lighting.emissive_color_gamma": "自发光颜色伽马", + "feature.linear_lighting.emissive_color_multiplier": "自发光颜色倍率", + "feature.linear_lighting.enable": "启用线性光照", + "feature.linear_lighting.fog_gamma": "雾伽马", + "feature.linear_lighting.fog_transparency_gamma": "雾透明度伽马", + "feature.linear_lighting.gamma_settings": "伽马设置", + "feature.linear_lighting.glowmap_gamma": "发光贴图伽马", + "feature.linear_lighting.glowmap_multiplier": "发光贴图倍率", + "feature.linear_lighting.key_feature_1": "可自定义的伽马校正", + "feature.linear_lighting.key_feature_2": "修正光照计算", + "feature.linear_lighting.key_feature_3": "使PBR真正生效", + "feature.linear_lighting.light_gamma": "光照伽马", + "feature.linear_lighting.membrane_effects_multiplier": "膜效果倍率", + "feature.linear_lighting.multipliers": "倍率", + "feature.linear_lighting.name": "线性光照", + "feature.linear_lighting.other_effects_multiplier": "其他效果倍率", + "feature.linear_lighting.point_light_multiplier": "点光源倍率", + "feature.linear_lighting.projected_effects_multiplier": "投射效果倍率", + "feature.linear_lighting.sky_gamma": "天空伽马", + "feature.linear_lighting.tab_advanced": "高级", + "feature.linear_lighting.tab_general": "通用", + "feature.linear_lighting.vanilla_diffuse_color_multiplier": "原版漫反射颜色倍率", + "feature.linear_lighting.vl_gamma": "体积光照伽马", + "feature.linear_lighting.water_gamma": "水伽马", + "feature.lod_blending.description": "在LOD对象与全细节对象之间提供无缝的视觉过渡,消除生硬的切换,创造平滑的视觉连续性。", + "feature.lod_blending.disable_terrain_vertex_colors": "禁用地形顶点颜色", + "feature.lod_blending.disable_terrain_vertex_colors_tooltip": "禁用附近地形上的顶点着色。建议与 xLODGen 生成、且 Vertex Color Intensity 设为 0 的地形 LOD 搭配使用。", + "feature.lod_blending.key_feature_1": "平滑的LOD对象亮度混合", + "feature.lod_blending.key_feature_2": "增强的地形LOD外观匹配", + "feature.lod_blending.key_feature_3": "针对雪景的LOD亮度调整", + "feature.lod_blending.key_feature_4": "可选的地形顶点颜色修改", + "feature.lod_blending.key_feature_5": "细节级别之间的无缝过渡", + "feature.lod_blending.lod_object_brightness": "LOD 物体亮度", + "feature.lod_blending.lod_object_gamma": "LOD 物体 Gamma", + "feature.lod_blending.lod_object_snow_brightness": "LOD 雪地物体亮度", + "feature.lod_blending.lod_object_snow_gamma": "LOD 雪地物体 Gamma", + "feature.lod_blending.lod_terrain_brightness": "LOD 地形亮度", + "feature.lod_blending.lod_terrain_gamma": "LOD 地形 Gamma", + "feature.lod_blending.name": "LOD混合", + "feature.perf_overlay.appearance": "外观", + "feature.perf_overlay.bg_opacity": "背景不透明度", + "feature.perf_overlay.clear_test_data": "清除测试数据", + "feature.perf_overlay.display_options": "显示选项", + "feature.perf_overlay.fps": "FPS:", + "feature.perf_overlay.frame_history_size": "帧历史大小", + "feature.perf_overlay.overlay_title": "性能叠加层", + "feature.perf_overlay.position": "位置:", + "feature.perf_overlay.post_fg_calculated": "帧生成后:计算计时(2倍帧生成前)", + "feature.perf_overlay.post_fg_fps": "帧生成后FPS:", + "feature.perf_overlay.post_fg_graph_tooltip": "FSR帧生成使用计算计时数据(2倍帧生成前)。\nDLSS帧生成提供测量计时数据。", + "feature.perf_overlay.raw_fps": "原始FPS:", + "feature.perf_overlay.reset_position": "重置位置", + "feature.perf_overlay.restore_defaults": "恢复默认值", + "feature.perf_overlay.restore_defaults_tooltip": "将性能叠加层设置恢复为默认值,包括图表、外观和更新间隔。", + "feature.perf_overlay.show_border": "显示边框", + "feature.perf_overlay.show_cs_passes": "显示CS渲染通道", + "feature.perf_overlay.show_draw_calls": "显示绘制调用", + "feature.perf_overlay.show_fps": "显示FPS计数器", + "feature.perf_overlay.show_frametime_graph": "显示帧时间图表", + "feature.perf_overlay.show_in_overlay": "在叠加层中显示", + "feature.perf_overlay.show_in_overlay_tooltip": "在单独的窗口中打开性能叠加层,即使主菜单关闭也保持打开。", + "feature.perf_overlay.show_post_fg_graph": "显示帧生成后帧时间图表", + "feature.perf_overlay.show_pre_fg_graph": "显示帧生成前帧时间图表", + "feature.perf_overlay.show_vram": "显示VRAM使用量", + "feature.perf_overlay.text_size": "文本大小", + "feature.perf_overlay.toggle_with": "切换键:", + "feature.perf_overlay.update_interval": "更新间隔", + "feature.perf_overlay.vram_not_available": "VRAM使用量:不可用", + "feature.perf_overlay.vram_usage": "VRAM使用量:", + "feature.performance_overlay.description": "实时性能监控系统,显示FPS、帧时间、绘制调用、显存使用量以及详细的着色器性能分析。", + "feature.performance_overlay.key_feature_1": "实时FPS和帧时间监控,可配置更新间隔", + "feature.performance_overlay.key_feature_2": "交互式绘制调用分析,按着色器类型展示性能细分", + "feature.performance_overlay.key_feature_3": "显存使用量监控,带可视化进度条", + "feature.performance_overlay.key_feature_4": "帧时间图表,用于帧生成前后的分析", + "feature.performance_overlay.key_feature_5": "A/B测试支持,对比不同配置的性能表现", + "feature.performance_overlay.key_feature_6": "颜色编码的性能指标,可自定义阈值", + "feature.performance_overlay.key_feature_7": "可移动的叠加窗口,位置持久保存", + "feature.performance_overlay.name": "性能叠加层", + "feature.render_doc.description": "提供应用内的RenderDoc捕获支持与便捷UI。", + "feature.render_doc.key_feature_1": "为捕获添加注释,可在RenderDoc UI中查看", + "feature.render_doc.key_feature_2": "打开捕获文件夹", + "feature.render_doc.key_feature_3": "捕获文件管理", + "feature.render_doc.name": "RenderDoc", + "feature.renderdoc.cancel": "取消", + "feature.renderdoc.capture_active": "RenderDoc捕获正在进行。", + "feature.renderdoc.capture_control": "捕获控制", + "feature.renderdoc.capture_control_tooltip": "手动捕获创建和基本控制", + "feature.renderdoc.capture_dir": "捕获目录:%s", + "feature.renderdoc.capture_dir_tooltip": "右键点击复制目录路径。", + "feature.renderdoc.capture_files": "捕获文件", + "feature.renderdoc.capture_files_tooltip": "查看和管理单个捕获文件", + "feature.renderdoc.capture_frames": "捕获帧数", + "feature.renderdoc.capture_frames_tooltip": "要捕获的连续帧数。1使用普通RenderDoc捕获;更高值使用TriggerMultiFrameCapture。", + "feature.renderdoc.capture_size": "捕获大小", + "feature.renderdoc.capture_size_tooltip": "捕获目录中所有捕获文件的总大小", + "feature.renderdoc.clear_all_captures": "清除所有捕获", + "feature.renderdoc.col_created": "创建时间", + "feature.renderdoc.col_filename": "文件名", + "feature.renderdoc.col_size": "大小", + "feature.renderdoc.comments_hint": "下一次捕获的附加注释(可选)", + "feature.renderdoc.comments_tooltip": "附加注释将追加到自动元数据中,并嵌入到.rdc文件中", + "feature.renderdoc.confirm_delete": "您确定要删除所有捕获文件吗?", + "feature.renderdoc.copy_dir_path": "复制目录路径", + "feature.renderdoc.create_capture": "创建捕获", + "feature.renderdoc.delete_size": "这将永久删除%u MB的捕获数据。", + "feature.renderdoc.disk_usage": "磁盘使用量", + "feature.renderdoc.disk_usage_tooltip": "监控捕获存储使用情况", + "feature.renderdoc.double_click_hint": "双击文件名以打开捕获文件", + "feature.renderdoc.enable_capture": "启用RenderDoc捕获", + "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", + "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", + "feature.renderdoc.hover_hint": "悬停在文件名上查看文件详情", + "feature.renderdoc.no_files": "未找到捕获文件。", + "feature.renderdoc.not_enough_space": "没有足够的可用磁盘空间来创建捕获。", + "feature.renderdoc.ok": "确定", + "feature.renderdoc.open_capture_dir": "打开捕获目录", + "feature.renderdoc.refresh_list": "刷新列表", + "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", + "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", + "feature.renderdoc.space_required": "至少需要{} MB的可用空间。", + "feature.renderdoc.yes_delete": "是,全部删除", + "feature.screen_space_gi.ao_only": "仅AO", + "feature.screen_space_gi.ao_power": "AO强度", + "feature.screen_space_gi.ao_radius": "AO半径", + "feature.screen_space_gi.ao_radius_tooltip": "较小的半径产生更紧密的AO。", + "feature.screen_space_gi.blur": "模糊", + "feature.screen_space_gi.blur_radius": "模糊半径", + "feature.screen_space_gi.buffer_viewer": "缓冲区查看器", + "feature.screen_space_gi.debug": "调试", + "feature.screen_space_gi.denoising": "降噪", + "feature.screen_space_gi.depth_fade_range": "深度渐隐范围", + "feature.screen_space_gi.depth_fade_range_tooltip": "基于深度的效果渐隐的距离范围。", + "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", + "feature.screen_space_gi.enabled": "启用", + "feature.screen_space_gi.enabled_tooltip": "启用屏幕空间全局光照。禁用时,所有其他设置将被忽略。", + "feature.screen_space_gi.extreme": "极致", + "feature.screen_space_gi.extreme_tooltip": "全分辨率且干净。", + "feature.screen_space_gi.full_res": "全分辨率", + "feature.screen_space_gi.geometry_weight": "几何权重", + "feature.screen_space_gi.geometry_weight_tooltip": "较高值使模糊对几何差异更敏感。", + "feature.screen_space_gi.half_res": "半分辨率", + "feature.screen_space_gi.hq_specular_il": "(实验性)HQ高光IL", + "feature.screen_space_gi.hq_specular_il_tooltip": "实验性的高光GI,更准确但需要更多采样。不会被模糊。", + "feature.screen_space_gi.il_distance_compensation": "IL距离补偿", + "feature.screen_space_gi.il_distance_compensation_tooltip": "增亮/调暗更远的辐射度采样。", + "feature.screen_space_gi.il_radius": "IL半径", + "feature.screen_space_gi.il_radius_tooltip": "较大的半径产生更宽的IL。", + "feature.screen_space_gi.il_saturation": "IL饱和度", + "feature.screen_space_gi.il_source_brightness": "IL源亮度", + "feature.screen_space_gi.indirect_lighting": "间接光照(IL)", + "feature.screen_space_gi.key_feature_1": "逼真的间接光照", + "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", + "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", + "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", + "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", + "feature.screen_space_gi.low": "低", + "feature.screen_space_gi.low_tooltip": "四分之一分辨率且模糊。", + "feature.screen_space_gi.max_frame_accumulation": "最大帧累积", + "feature.screen_space_gi.max_frame_accumulation_tooltip": "累积多少过去帧的结果。较高值噪点更少但可能导致鬼影。", + "feature.screen_space_gi.min_screen_radius": "最小屏幕半径", + "feature.screen_space_gi.min_screen_radius_tooltip": "以显示宽度比例表示的最小屏幕空间效果半径,防止远场AO过小。", + "feature.screen_space_gi.movement_disocclusion": "运动去遮挡", + "feature.screen_space_gi.movement_disocclusion_tooltip": "如果像素从上帧移动得太远,其辐射度将不会带入此帧。\n较低值更严格。", + "feature.screen_space_gi.name": "屏幕空间GI", + "feature.screen_space_gi.quality_performance": "质量/性能", + "feature.screen_space_gi.quarter_res": "四分之一分辨率", + "feature.screen_space_gi.reference": "参考", + "feature.screen_space_gi.reference_tooltip": "参考模式。", + "feature.screen_space_gi.shader_compile_error": "计算着色器编译失败!", + "feature.screen_space_gi.show_advanced": "显示高级选项", + "feature.screen_space_gi.slices": "切片", + "feature.screen_space_gi.slices_tooltip": "采样采用多少个方向。\n控制噪点。", + "feature.screen_space_gi.standard": "标准", + "feature.screen_space_gi.standard_tooltip": "半分辨率且相对稳定。", + "feature.screen_space_gi.steps_per_slice": "每切片步数", + "feature.screen_space_gi.steps_per_slice_tooltip": "在每个方向上采样的数量。\n控制光照精度,以及效果半径较大时的噪点。", + "feature.screen_space_gi.temporal_denoiser": "时间降噪器", + "feature.screen_space_gi.thickness": "厚度", + "feature.screen_space_gi.thickness_tooltip": "遮挡物的厚度。仅影响AO。", + "feature.screen_space_gi.toggles": "开关", + "feature.screen_space_gi.vanilla_ssao": "原版SSAO", + "feature.screen_space_gi.vanilla_ssao_tooltip": "启用Skyrim内置的SSAO。使用SSGI时通常禁用此选项以避免双重变暗。", + "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "VR不支持原版SSAO。", + "feature.screen_space_gi.view_resize": "视图调整大小", + "feature.screen_space_gi.visual": "视觉", + "feature.screen_space_gi.visual_il": "视觉 - IL", + "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", + "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", + "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", + "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", + "feature.screen_space_shadows.enable": "启用", + "feature.screen_space_shadows.enable_tooltip": "启用来自太阳/月亮方向的屏幕空间接触阴影。", + "feature.screen_space_shadows.general": "通用", + "feature.screen_space_shadows.key_feature_1": "增强的接触阴影", + "feature.screen_space_shadows.key_feature_2": "提升阴影细节", + "feature.screen_space_shadows.key_feature_3": "更好的阴影精度", + "feature.screen_space_shadows.key_feature_4": "精细尺度的阴影效果", + "feature.screen_space_shadows.key_feature_5": "可配置的阴影对比度", + "feature.screen_space_shadows.name": "屏幕空间阴影", + "feature.screen_space_shadows.sample_count": "采样数量倍率", + "feature.screen_space_shadows.sample_count_tooltip": "阴影射线采样数量的倍率。较高值以性能为代价增加阴影范围。适应渲染分辨率。", + "feature.screen_space_shadows.shadow_contrast": "阴影对比度", + "feature.screen_space_shadows.shadow_contrast_tooltip": "阴影过渡的对比度增强。较高值产生更硬的阴影边缘。", + "feature.screen_space_shadows.surface_thickness": "表面厚度", + "feature.screen_space_shadows.surface_thickness_tooltip": "阴影检测的假设表面厚度。较低值产生更薄、更精确的阴影。", + "feature.screen_space_shadows.vr_stereo_sync": "VR立体同步", + "feature.screen_space_shadows.vr_stereo_sync_tooltip": "通过双向重投影同步左右眼之间的阴影数据,并应用深度加权模糊以减少每只眼睛的噪点。使用最小混合,如果任一眼睛检测到遮挡物,则保留阴影。", + "feature.screenshot.apply_crop": "应用裁剪", + "feature.screenshot.async_note": "捕获和保存异步运行,不会让游戏停顿。", + "feature.screenshot.crop": "裁剪", + "feature.screenshot.folder": "文件夹", + "feature.screenshot.folder_tooltip": "相对路径相对于Skyrim安装目录解析。\n绝对路径(例如D:\\Captures)直接保存到该位置。", + "feature.screenshot.hdr_bit_depth": "HDR PNG 位深度", + "feature.screenshot.hdr_bit_depth_tooltip": "48 bpp RGB PNG 负载的量化位深。11位是较好的默认值;更高的值会增加文件大小,但收益递减。", + "feature.screenshot.hdr_note": "HDR 已启用:将显示帧保存为带有 HDR10 元数据的 PNG(48 bpp RGB,cICP/cLLi)。请使用支持 HDR 的查看器,如 Windows 照片(HDR 开启)或 Special K SKIF。", + "feature.screenshot.hotkey": "热键", + "feature.screenshot.hotkey_collision": "此热键与原版PrintScreen冲突;两者都会触发保存。在Skyrim.ini中设置bAllowScreenShot=0以抑制原版,或在上方选择不同的热键。", + "feature.screenshot.name": "截图", + "feature.screenshot.open": "打开", + "feature.screenshot.output": "输出", + "feature.screenshot.sdr_note": "启用HDR显示来捕捉有着HDR10元数据的HDR PNG截图。SDR和VR截图会使用以下选择的无损格式。", + "feature.screenshot.take_screenshot": "立即截图", + "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "原版镜面反射贴图倍率,应用到第一层粗糙度。", + "feature.skin.adds_a_constant_layer_of_wetness_to_all": "为所有皮肤添加一层恒定湿润度,使其始终略显潮湿或出汗,即使角色不在水中或没有剧烈活动。", + "feature.skin.advanced_skin_shader_using_dual_specular_lobes": "使用双镜面反射叶瓣的高级皮肤着色器。", + "feature.skin.base_color_multiplier": "基础色倍率", + "feature.skin.body_tiling_multiplier": "身体平铺倍率", + "feature.skin.controls_how_bumpy_wet_skin_appears_higher_values": "控制湿皮肤看起来有多凹凸。较高的值会让湿润区域出现更明显的表面波纹和扭曲。", + "feature.skin.controls_how_much_fine_detail_is_added_to": "控制添加到湿润图案中的细节量。较高的值会在基础图案上叠加更多小尺度变化。", + "feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer": "控制角质层的微观粗糙度。", + "feature.skin.controls_the_overall_contrast_and_roughness_of_the": "控制湿润图案的整体对比度和粗糙度。较高的值会让图案更明显、变化更多。", + "feature.skin.controls_the_size_of_the_wet_dry_pattern": "控制皮肤上干湿图案的大小。较高的值会产生更细、更详细的图案;较低的值会产生更大、更宽的湿斑。", + "feature.skin.description": "高级皮肤通过多种技术增强角色皮肤渲染。", + "feature.skin.dynamic_wetness_detected": "检测到动态湿润度。", + "feature.skin.enable_advanced_skin": "启用高级皮肤", + "feature.skin.enable_skin_detail": "启用皮肤细节", + "feature.skin.enable_skin_detail_texture": "启用皮肤细节纹理", + "feature.skin.enable_sss_transmission": "启用 SSS 透射", + "feature.skin.extra_edge_roughness": "额外边缘粗糙度", + "feature.skin.extra_roughness_at_the_edges_of_the_skin": "皮肤边缘的额外粗糙度,用于近似脸部细绒毛。", + "feature.skin.extra_skin_wetness": "额外皮肤湿润度", + "feature.skin.fresnel_f0": "菲涅尔 F0", + "feature.skin.fresnel_reflectance": "菲涅尔反射率", + "feature.skin.full_sweat_threshold": "满汗阈值", + "feature.skin.fuzz_f0": "细绒毛 F0", + "feature.skin.fuzz_roughness": "细绒毛粗糙度", + "feature.skin.fuzz_strength": "细绒毛强度", + "feature.skin.how_many_seconds_it_takes_for_skin_to": "离开水后皮肤完全变干所需的秒数。较高的值会让湿润持续更久。", + "feature.skin.intensity_of_secondary_specular_highlights": "次级镜面反射高光强度。", + "feature.skin.key_feature_1": "基于物理的双镜面反射叶瓣,提供更真实的皮肤高光", + "feature.skin.key_feature_2": "平铺皮肤细节纹理,提升真实感", + "feature.skin.key_feature_3": "支持额外的粗糙度、半透明和湿润度纹理", + "feature.skin.key_feature_4": "重做湿润系统,用于动态皮肤效果", + "feature.skin.multiplier_for_specular_map": "镜面反射贴图倍率", + "feature.skin.multiplier_for_the_base_color_texture": "基础色纹理倍率。", + "feature.skin.multiply_the_tiling_for_the_body_to_match": "将身体平铺倍率乘上该值以匹配脸部。", + "feature.skin.name": "高级皮肤", + "feature.skin.options_for_additional_roughness_and_specular_maps": "额外粗糙度和镜面反射贴图选项。", + "feature.skin.physical_main_roughness_multiplier": "物理主粗糙度倍率", + "feature.skin.physical_second_roughness_multiplier": "物理次粗糙度倍率", + "feature.skin.physical_specular_multiplier": "物理镜面反射倍率", + "feature.skin.primary_roughness": "主粗糙度", + "feature.skin.reload_skin_detail_texture": "重新加载皮肤细节纹理", + "feature.skin.secondary_roughness": "次粗糙度", + "feature.skin.secondary_specular_strength": "次级镜面反射强度", + "feature.skin.should_be_30_50_lower_than_primary": "应比主粗糙度低 30-50%%。", + "feature.skin.skin_detail_strength": "皮肤细节强度", + "feature.skin.skin_detail_tiling": "皮肤细节平铺", + "feature.skin.smoothness_of_epidermal_cell_layer_reflections": "表皮细胞层反射的平滑度。", + "feature.skin.specular_texture_multiplier": "镜面反射纹理倍率", + "feature.skin.sss_width": "SSS 宽度", + "feature.skin.stamina_threshold_for_sweat": "出汗耐力阈值", + "feature.skin.strength_of_skin_detail_texture": "皮肤细节纹理强度。", + "feature.skin.the_character_reaches_maximum_sweat_when_stamina_drops": "当耐力低于此百分比时,角色达到最大出汗量。例如 0.15 表示耐力低于 15%% 时满汗。", + "feature.skin.the_character_starts_sweating_when_their_stamina_drops": "当耐力低于此百分比时,角色开始出汗。例如 0.75 表示耐力低于 75%% 时出现汗水。", + "feature.skin.the_more_tiling_the_more_detailed_the_skin": "平铺越多,皮肤细节越丰富。", + "feature.skin.translucency": "半透明度", + "feature.skin.translucency_of_the_sss_transmittance_effect": "SSS 透射效果的半透明度。", + "feature.skin.use_dynamic_wetness": "使用动态湿润度", + "feature.skin.wetness_fade_out_time": "湿润度淡出时间", + "feature.skin.wetness_normal_scale": "湿润法线强度", + "feature.skin.wetness_perlin_noise_lacunarity": "湿润 Perlin 噪声频率倍增", + "feature.skin.wetness_perlin_noise_persistence": "湿润 Perlin 噪声持续度", + "feature.skin.wetness_perlin_noise_scale": "湿润 Perlin 噪声比例", + "feature.skin.width_of_the_sss_transmittance_effect": "SSS 透射效果的宽度。", + "feature.sky_sync.custom_angle": "自定义角度", + "feature.sky_sync.custom_angle_tooltip": "设置太阳轨迹的自定义角度。", + "feature.sky_sync.description": "将体积光照和阴影与天空中太阳和月亮的实际位置同步。", + "feature.sky_sync.enabled": "启用", + "feature.sky_sync.enabled_tooltip": "启用或禁用天空同步功能。", + "feature.sky_sync.key_feature_1": "修复太阳/月亮位置与光照方向不匹配的问题", + "feature.sky_sync.key_feature_2": "包含可配置的替代太阳路径,呈现更逼真戏剧化的光照", + "feature.sky_sync.key_feature_3": "根据可见性在太阳和月亮之间平滑切换光源", + "feature.sky_sync.key_feature_4": "月光源可在Masser、Secunda或最亮者之间切换", + "feature.sky_sync.key_feature_5": "基于月相自动计算月光强度", + "feature.sky_sync.key_feature_6": "修复玩家提升海拔时太阳在地平线上显得更高的问题", + "feature.sky_sync.min_shadow_elevation": "最小阴影仰角", + "feature.sky_sync.min_shadow_elevation_tooltip": "阳光设置的最小角度。限制阴影长度。更高 = 日落/日出时更短的阴影。", + "feature.sky_sync.moon_light_source": "月亮光源", + "feature.sky_sync.moon_light_source_brightest": "最亮者", + "feature.sky_sync.moon_light_source_masser": "Masser", + "feature.sky_sync.moon_light_source_secunda": "Secunda", + "feature.sky_sync.moon_light_source_tooltip": "选择夜晚投射阴影的月亮。", + "feature.sky_sync.name": "天空同步", + "feature.sky_sync.sun_path": "太阳路径", + "feature.sky_sync.sun_path_custom": "自定义", + "feature.sky_sync.sun_path_northern": "北侧天空", + "feature.sky_sync.sun_path_southern": "南侧天空", + "feature.sky_sync.sun_path_tooltip": "选择太阳穿越天空的轨迹。", + "feature.sky_sync.sun_path_vanilla": "原版", + "feature.sky_sync.sun_position_offsets": "太阳位置偏移", + "feature.sky_sync.sun_position_offsets_desc": "在日出/日落时移动太阳高度。重置天气以查看更改。", + "feature.sky_sync.sunrise_begin": "日出开始(小时)", + "feature.sky_sync.sunrise_begin_tooltip": "太阳开始升起的时间偏移。", + "feature.sky_sync.sunrise_end": "日出结束(小时)", + "feature.sky_sync.sunrise_end_tooltip": "太阳完成升起的时间偏移。", + "feature.sky_sync.sunset_begin": "日落开始(小时)", + "feature.sky_sync.sunset_begin_tooltip": "太阳开始落下的时间偏移。", + "feature.sky_sync.sunset_end": "日落结束(小时)", + "feature.sky_sync.sunset_end_tooltip": "太阳完成落下的时间偏移。", + "feature.sky_sync.use_alternate_sun_path": "使用备用太阳路径", + "feature.sky_sync.use_alternate_sun_path_tooltip": "根据时间和季节计算太阳位置,而非原版运动。", + "feature.skylighting.description": "通过计算天空遮蔽和方向光照,模拟逼真的环境照明,在户外环境中提供更精确自然的照明。", + "feature.skylighting.diffuse_min_visibility": "漫反射最小可见度", + "feature.skylighting.key_feature_1": "天空遮蔽计算,用于环境光照", + "feature.skylighting.key_feature_2": "基于环境几何体的方向性天空光照", + "feature.skylighting.key_feature_3": "增强的户外场景环境照明", + "feature.skylighting.key_feature_4": "支持变化的天空光照强度", + "feature.skylighting.key_feature_5": "与现有光照系统集成", + "feature.skylighting.max_zenith": "最大天顶角", + "feature.skylighting.max_zenith_tooltip": "较小的角度产生更集中的自上而下阴影。", + "feature.skylighting.min_visibility_desc": "最小可见度值。漫反射使物体变暗。镜面反射从反射中移除天空。", + "feature.skylighting.name": "天空光照", + "feature.skylighting.rebuild": "重建天光", + "feature.skylighting.rebuild_tooltip": "以下更改需要重建、加载屏幕或离开当前位置才能应用。", + "feature.skylighting.specular_min_visibility": "镜面反射最小可见度", + "feature.sss.base_profile": "基础预设", + "feature.sss.blur_radius": "模糊半径", + "feature.sss.blur_radius_tooltip": "模糊半径。", + "feature.sss.burley": "Burley", + "feature.sss.burley_samples": "Burley采样数", + "feature.sss.enable_character_lighting": "启用角色光照", + "feature.sss.enable_character_lighting_tooltip": "原版功能,不推荐。", + "feature.sss.falloff": "衰减", + "feature.sss.human_profile": "人类预设", + "feature.sss.mean_free_path_color": "平均自由路径颜色", + "feature.sss.mean_free_path_color_tooltip": "控制光在红色、绿色和蓝色通道中进入次表面的距离。由平均自由路径距离缩放。", + "feature.sss.mean_free_path_distance": "平均自由路径距离", + "feature.sss.mean_free_path_distance_tooltip": "控制平均自由路径颜色进入次表面的距离。", + "feature.sss.separable_sss": "可分离SSS", + "feature.sss.settings": "设置", + "feature.sss.strength": "强度", + "feature.sss.thickness": "厚度", + "feature.sss.thickness_tooltip": "相对于深度的模糊半径。", + "feature.subsurface_scattering.description": "模拟光线穿透半透明材质(如皮肤),创造更逼真的角色光照效果。\n该技术使有机材质看起来更生动自然。", + "feature.subsurface_scattering.key_feature_1": "逼真的皮肤光照", + "feature.subsurface_scattering.key_feature_2": "光穿透模拟", + "feature.subsurface_scattering.key_feature_3": "为不同材质提供独立的配置文件", + "feature.subsurface_scattering.key_feature_4": "增强的角色外观", + "feature.subsurface_scattering.key_feature_5": "可配置的散射属性", + "feature.subsurface_scattering.name": "次表面散射", + "feature.terrain_blending.description": "提供地形与物体之间的无缝混合,消除物体与地面交汇处的生硬过渡,呈现更自然的景观。", + "feature.terrain_blending.enable": "启用地形混合", + "feature.terrain_blending.enable_tooltip": "启用地形与物体之间的无缝混合。", + "feature.terrain_blending.key_feature_1": "地形与物体的无缝混合过渡", + "feature.terrain_blending.key_feature_2": "高级深度缓冲区处理,实现平滑集成", + "feature.terrain_blending.key_feature_3": "支持替代地形渲染模式", + "feature.terrain_blending.key_feature_4": "针对复杂场景的多通道渲染优化", + "feature.terrain_blending.key_feature_5": "增强的地形交互视觉连续性", + "feature.terrain_blending.name": "地形混合", + "feature.terrain_helper.description": "为需要额外纹理槽和视差映射功能的地形模组提供增强的地形材质支持。", + "feature.terrain_helper.key_feature_1": "扩展的地形材质纹理槽支持", + "feature.terrain_helper.key_feature_2": "地形纹理的视差映射集成", + "feature.terrain_helper.key_feature_3": "自动地形材质检测与设置", + "feature.terrain_helper.key_feature_4": "支持高级地形修改", + "feature.terrain_helper.key_feature_5": "地形增强模组的兼容层", + "feature.terrain_helper.name": "地形辅助", + "feature.terrain_shadows.buffer_viewer": "缓冲区查看器", + "feature.terrain_shadows.debug": "调试", + "feature.terrain_shadows.description": "使用高度图数据为地形特征添加逼真的阴影投射,创造准确的地形阴影,增强深度感知和视觉真实感。", + "feature.terrain_shadows.enable_terrain_shadow": "启用地形阴影", + "feature.terrain_shadows.key_feature_1": "基于高度图的地形阴影计算", + "feature.terrain_shadows.key_feature_2": "基于太阳位置的动态阴影更新", + "feature.terrain_shadows.key_feature_3": "支持自定义高度图文件", + "feature.terrain_shadows.key_feature_4": "实时阴影预处理和计算", + "feature.terrain_shadows.key_feature_5": "与现有阴影系统集成", + "feature.terrain_shadows.name": "地形阴影", + "feature.terrain_variation.apply_to_lod_terrain": "应用到 LOD 地形", + "feature.terrain_variation.apply_to_lod_terrain_tooltip": "将该平铺修复应用到 LOD 地形对象。\n这有助于减少远处地形上可见的重复平铺效果。", + "feature.terrain_variation.description": "减少地形纹理的重复图案效果。\n通过为纹理采样添加变化来创造更自然的地形外观。", + "feature.terrain_variation.enable_tiling_fix": "启用地形平铺修复", + "feature.terrain_variation.enable_tiling_fix_tooltip": "减少地形纹理的重复平铺感。\n该技术通过在纹理采样中加入变化,让地形看起来更自然。", + "feature.terrain_variation.key_feature_1": "减少地形纹理的平铺感", + "feature.terrain_variation.key_feature_2": "可调节的基于距离的混合", + "feature.terrain_variation.key_feature_3": "提升地形视觉质量", + "feature.terrain_variation.key_feature_4": "与扩展材质视差兼容", + "feature.terrain_variation.name": "地形变化", + "feature.true_pbr.base_color_scale": "基础颜色缩放", + "feature.true_pbr.blue": "蓝", + "feature.true_pbr.coat": "镀层", + "feature.true_pbr.coat_color": "镀层颜色", + "feature.true_pbr.coat_roughness": "镀层粗糙度", + "feature.true_pbr.coat_specular_level": "镀层高光等级", + "feature.true_pbr.coat_strength": "镀层强度", + "feature.true_pbr.density_randomization": "密度随机化", + "feature.true_pbr.displacement_scale": "位移缩放", + "feature.true_pbr.enabled": "启用", + "feature.true_pbr.glint": "闪烁高光", + "feature.true_pbr.global_settings": "全局设置", + "feature.true_pbr.green": "绿", + "feature.true_pbr.inner_layer_displacement_offset": "内层位移偏移", + "feature.true_pbr.log_microfacet_density": "微表面密度对数", + "feature.true_pbr.material_density_randomization": "密度随机化", + "feature.true_pbr.material_glint": "闪烁高光", + "feature.true_pbr.material_glint_enabled": "启用", + "feature.true_pbr.material_log_microfacet_density": "微表面密度对数", + "feature.true_pbr.material_microfacet_roughness": "微表面粗糙度", + "feature.true_pbr.material_object": "材质对象", + "feature.true_pbr.material_object_settings": "材质对象设置", + "feature.true_pbr.material_save": "保存", + "feature.true_pbr.material_screenspace_scale": "屏幕空间缩放", + "feature.true_pbr.material_specular_level": "高光等级", + "feature.true_pbr.microfacet_roughness": "微表面粗糙度", + "feature.true_pbr.name": "True PBR", + "feature.true_pbr.red": "红", + "feature.true_pbr.reset_to_1_0": "重置为 1.0", + "feature.true_pbr.roughness": "粗糙度", + "feature.true_pbr.roughness_scale": "粗糙度缩放", + "feature.true_pbr.save": "保存", + "feature.true_pbr.screenspace_scale": "屏幕空间缩放", + "feature.true_pbr.specular_level": "高光等级", + "feature.true_pbr.subsurface": "次表面", + "feature.true_pbr.subsurface_color": "次表面颜色", + "feature.true_pbr.subsurface_opacity": "次表面不透明度", + "feature.true_pbr.texture_set": "纹理集", + "feature.true_pbr.texture_set_settings": "纹理集设置", + "feature.true_pbr.vertex_ao_strength": "顶点 AO 强度", + "feature.unified_water.debug": "调试", + "feature.unified_water.description": "通过用LOD0(近景水面)替换远处水面瓦片,提供全面的水面LOD不匹配修复。", + "feature.unified_water.error_water_cache_generation_failed_for_worldspaces_check": "错误:%d 个世界空间的水面缓存生成失败。请检查安装和 CommunityShaders.log", + "feature.unified_water.generating_water_cache": "正在生成水面缓存:", + "feature.unified_water.key_feature_1": "统一远景和近景水面的外观,统一所有光照视觉效果。", + "feature.unified_water.key_feature_2": "彻底且根本地解决水面LOD不匹配问题。", + "feature.unified_water.key_feature_3": "提供水面几何渲染的后台系统,支持更高级的水面效果。", + "feature.unified_water.key_feature_4": "通过使用优化的远距离水面网格来提升原版性能。", + "feature.unified_water.name": "统一水面", + "feature.unified_water.regenerate_caches": "重新生成缓存", + "feature.unified_water.regenerate_flowmap": "重新生成流图", + "feature.unified_water.use_optimised_meshes": "使用优化网格", + "feature.unified_water.use_optimised_meshes_tooltip": "使用三角面数显著更低的网格以提升性能,视觉质量无损。\n仅影响新创建的水体 - 需要切换位置或重启游戏才能生效。", + "feature.upscaling.backend_diagnostics": "后端诊断", + "feature.upscaling.description": "先进的超分辨率和帧生成技术,提升游戏性能。", + "feature.upscaling.dlss_model_preset": "DLSS模型预设", + "feature.upscaling.dlss_model_preset_default": "默认", + "feature.upscaling.dlss_model_preset_j": "预设 J", + "feature.upscaling.dlss_model_preset_k": "预设 K", + "feature.upscaling.dlss_model_preset_l": "预设 L", + "feature.upscaling.dlss_model_preset_m": "预设 M", + "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", + "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", + "feature.upscaling.fps_limit": "FPS限制", + "feature.upscaling.fps_limit_tooltip_1": "设置帧率上限目标。", + "feature.upscaling.fps_limit_tooltip_2": "起始值设置为比刷新率低2-3 FPS(例如120 Hz为117)。", + "feature.upscaling.frame_generation": "帧生成", + "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", + "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", + "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", + "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", + "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", + "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", + "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", + "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", + "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", + "feature.upscaling.key_feature_1": "DLSS(深度学习超采样)支持", + "feature.upscaling.key_feature_2": "FSR(FidelityFX超分辨率)支持", + "feature.upscaling.key_feature_3": "TAA(时间抗锯齿)支持", + "feature.upscaling.key_feature_4": "支持的系统可启用帧生成", + "feature.upscaling.low_latency_boost": "低延迟增强", + "feature.upscaling.low_latency_boost_tooltip_1": "保持GPU时钟更高,避免低GPU负载时的延迟尖峰。", + "feature.upscaling.low_latency_boost_tooltip_2": "在帧时间跳跃时有帮助;但会增加功耗和发热。", + "feature.upscaling.low_latency_mode": "低延迟模式", + "feature.upscaling.low_latency_mode_tooltip_1": "通过将CPU工作更紧密地与GPU同步来减少输入延迟。", + "feature.upscaling.low_latency_mode_tooltip_2": "可能略微降低最大FPS,但通常感觉响应更快。", + "feature.upscaling.marker_optimization_unavailable": "标记优化不可用(PCL未加载)。", + "feature.upscaling.method": "方法", + "feature.upscaling.method_none": "无", + "feature.upscaling.method_taa": "TAA", + "feature.upscaling.name": "超分辨率", + "feature.upscaling.native_inputs": "原生输入", + "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", + "feature.upscaling.preset_balanced": "平衡", + "feature.upscaling.preset_dlaa": "DLAA", + "feature.upscaling.preset_native_aa": "原生抗锯齿", + "feature.upscaling.preset_performance": "性能", + "feature.upscaling.preset_quality": "质量", + "feature.upscaling.preset_ultra_performance": "超级性能", + "feature.upscaling.reflex_blocked_by_fg": "当DX12帧生成交换链激活时,Reflex不可用。", + "feature.upscaling.reflex_not_available": "Reflex不可用。请确保sl.reflex.dll存在并重启。", + "feature.upscaling.sharpness": "锐度", + "feature.upscaling.streamline_logging": "Streamline日志记录", + "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", + "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", + "feature.upscaling.upscale_preset": "升频预设", + "feature.upscaling.upscaling_intermediates": "升频中间结果", + "feature.upscaling.use_fps_limit": "使用FPS限制", + "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", + "feature.upscaling.use_fps_limit_tooltip_2": "相比无上限渲染可以降低延迟。", + "feature.upscaling.use_markers_to_optimize": "使用标记优化", + "feature.upscaling.use_markers_to_optimize_tooltip_1": "使用帧标记以实现更紧密的Reflex计时。", + "feature.upscaling.use_markers_to_optimize_tooltip_2": "先尝试开启;如果在您的设置上造成卡顿则关闭。", + "feature.upscaling.view_resize": "视图调整大小", + "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", + "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", + "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", + "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", + "feature.volumetric_lighting.exterior_depth": "室外深度", + "feature.volumetric_lighting.exterior_height": "室外高度", + "feature.volumetric_lighting.exterior_quality": "室外质量", + "feature.volumetric_lighting.exterior_width": "室外宽度", + "feature.volumetric_lighting.interior_depth": "室内深度", + "feature.volumetric_lighting.interior_height": "室内高度", + "feature.volumetric_lighting.interior_quality": "室内质量", + "feature.volumetric_lighting.interior_width": "室内宽度", + "feature.volumetric_lighting.key_feature_1": "逼真的光散射", + "feature.volumetric_lighting.key_feature_2": "上帝射线和大气效果", + "feature.volumetric_lighting.key_feature_3": "独立的室内/室外设置", + "feature.volumetric_lighting.key_feature_4": "可配置的质量等级", + "feature.volumetric_lighting.key_feature_5": "增强的大气沉浸感", + "feature.volumetric_lighting.name": "体积光照", + "feature.volumetric_lighting.quality_custom": "自定义", + "feature.volumetric_lighting.quality_high": "高", + "feature.volumetric_lighting.quality_low": "低", + "feature.volumetric_lighting.quality_medium": "中", + "feature.volumetric_shadows.description": "为粒子和贴花等效果提供降采样的VSM阴影贴图。\n以最小的性能影响改善透明对象上的阴影质量。", + "feature.volumetric_shadows.key_feature_1": "降采样的VSM阴影", + "feature.volumetric_shadows.key_feature_2": "高斯模糊滤波", + "feature.volumetric_shadows.key_feature_3": "多级联支持", + "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", + "feature.volumetric_shadows.name": "体积阴影", + "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", + "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", + "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", + "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", + "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", + "feature.vr.key_feature_5": "可配置的遮挡剔除参数", + "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "调试", + "feature.vr_stereo.debug_pom_depth": "调试POM深度", + "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", + "feature.vr_stereo.enable": "启用", + "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", + "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", + "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", + "feature.vr_stereo.full_blend_distance": "完全混合距离", + "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", + "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", + "feature.vr_stereo.off": "关闭", + "feature.vr_stereo.pom_depth_scale": "POM深度缩放", + "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", + "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", + "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", + "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", + "feature.water_effects.key_feature_1": "逼真的水面焦散", + "feature.water_effects.key_feature_2": "增强的水下光照", + "feature.water_effects.key_feature_3": "水面上的动态光影图案", + "feature.water_effects.key_feature_4": "提升水面视觉保真度", + "feature.water_effects.key_feature_5": "大气水下效果", + "feature.water_effects.name": "水面效果", + "feature.wetness_effects.advanced": "高级", + "feature.wetness_effects.breadth": "广度", + "feature.wetness_effects.chance": "概率", + "feature.wetness_effects.chance_tooltip": "实际产生飞溅和涟漪的雨滴比例。较高的值会增加效果密度,但对性能影响最小。", + "feature.wetness_effects.climate_arctic_detail_0": "寒冷干燥的气候,降水量极少。", + "feature.wetness_effects.climate_arctic_detail_1": "最大降水量:约1.08毫米/小时(小雨)", + "feature.wetness_effects.climate_arctic_detail_2": "倍率:湿润度0.5倍,积水0.3倍,转换0.5倍。", + "feature.wetness_effects.climate_arctic_detail_3": "雨滴:30%概率,网格3.5单位,间隔0.4秒。", + "feature.wetness_effects.climate_arctic_detail_4": "性能影响:极低", + "feature.wetness_effects.climate_arctic_effect_0": "缓慢湿润累积(0.5倍)", + "feature.wetness_effects.climate_arctic_effect_1": "极少量积水形成(0.3倍)", + "feature.wetness_effects.climate_arctic_effect_2": "缓慢天气转换(0.5倍)", + "feature.wetness_effects.climate_arctic_effect_3": "稀疏降水(30%概率)", + "feature.wetness_effects.climate_coastal_detail_0": "海洋性气候,降水量大且频繁。", + "feature.wetness_effects.climate_coastal_detail_1": "最大降水量:约8.06毫米/小时(大雨)", + "feature.wetness_effects.climate_coastal_detail_2": "倍率:湿润度1.5倍,积水1.7倍,转换1.7倍。", + "feature.wetness_effects.climate_coastal_detail_3": "雨滴:80%概率,网格2.5单位,间隔0.25秒。", + "feature.wetness_effects.climate_coastal_detail_4": "性能影响:中等", + "feature.wetness_effects.climate_coastal_effect_0": "快速湿润累积(1.5倍)", + "feature.wetness_effects.climate_coastal_effect_1": "增强积水形成(1.7倍)", + "feature.wetness_effects.climate_coastal_effect_2": "快速天气转换(1.7倍)", + "feature.wetness_effects.climate_coastal_effect_3": "频繁降雨事件(80%概率)", + "feature.wetness_effects.climate_legacy_detail_0": "Riverwood的原始雨水效果值,提供完全向后兼容。", + "feature.wetness_effects.climate_legacy_detail_1": "最大降水量:约0.66毫米/小时(极小雨)", + "feature.wetness_effects.climate_legacy_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", + "feature.wetness_effects.climate_legacy_detail_3": "雨滴:30%概率,网格4.0单位,间隔0.5秒。", + "feature.wetness_effects.climate_legacy_detail_4": "性能影响:极低(基准线)", + "feature.wetness_effects.climate_legacy_effect_0": "原始湿润累积(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_1": "原始积水形成(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_2": "原始天气转换(1.0倍)", + "feature.wetness_effects.climate_legacy_effect_3": "原始雨滴频率(1.0倍)", + "feature.wetness_effects.climate_monsoon_detail_0": "热带/季风气候,极端降水量。", + "feature.wetness_effects.climate_monsoon_detail_1": "最大降水量:约22毫米/小时(极端)", + "feature.wetness_effects.climate_monsoon_detail_2": "倍率:湿润度2.0倍,积水2.5倍,转换2.0倍。", + "feature.wetness_effects.climate_monsoon_detail_3": "雨滴:100%概率,网格2.0单位,间隔0.2秒。", + "feature.wetness_effects.climate_monsoon_detail_4": "天际的小雨将无法匹配湿润效果。", + "feature.wetness_effects.climate_monsoon_detail_5": "性能影响:高(可能影响GPU性能)", + "feature.wetness_effects.climate_monsoon_effect_0": "极速湿润累积(2.0倍)", + "feature.wetness_effects.climate_monsoon_effect_1": "最大积水形成(2.5倍)", + "feature.wetness_effects.climate_monsoon_effect_2": "极动态天气(2.0倍)", + "feature.wetness_effects.climate_monsoon_effect_3": "最大雨滴频率(100%概率)", + "feature.wetness_effects.climate_nordic_detail_0": "平衡的温带北欧气候。", + "feature.wetness_effects.climate_nordic_detail_1": "最大降水量:约3.35毫米/小时(中雨)", + "feature.wetness_effects.climate_nordic_detail_2": "倍率:湿润度1.0倍,积水1.0倍,转换1.0倍。", + "feature.wetness_effects.climate_nordic_detail_3": "雨滴:100%概率,网格3.0单位,间隔1.0秒。", + "feature.wetness_effects.climate_nordic_detail_4": "性能影响:低", + "feature.wetness_effects.climate_nordic_effect_0": "标准湿润累积(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_1": "标准积水形成(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_2": "标准天气转换(1.0倍)", + "feature.wetness_effects.climate_nordic_effect_3": "中等雨滴频率(100%概率)", + "feature.wetness_effects.climate_preset": "气候预设", + "feature.wetness_effects.climate_preset_arctic": "北极苔原", + "feature.wetness_effects.climate_preset_arctic_desc": "寒冷干燥的北极气候(小雨)", + "feature.wetness_effects.climate_preset_coastal": "温带沿海", + "feature.wetness_effects.climate_preset_coastal_desc": "海洋性气候(大雨)", + "feature.wetness_effects.climate_preset_custom": "自定义", + "feature.wetness_effects.climate_preset_custom_desc": "用户自定义设置", + "feature.wetness_effects.climate_preset_legacy": "旧版", + "feature.wetness_effects.climate_preset_legacy_desc": "原始雨水效果值(极小雨)", + "feature.wetness_effects.climate_preset_monsoon": "季风/极端", + "feature.wetness_effects.climate_preset_monsoon_desc": "极端季风气候(暴雨)", + "feature.wetness_effects.climate_preset_nordic": "北欧(默认)", + "feature.wetness_effects.climate_preset_nordic_desc": "平衡的北欧气候(中雨)", + "feature.wetness_effects.climate_preset_unknown": "未知", + "feature.wetness_effects.climate_presets": "气候预设", + "feature.wetness_effects.current_climate_preset": "当前气候预设", + "feature.wetness_effects.custom_preset_tooltip_0": "自定义设置 - 您已修改预设值。", + "feature.wetness_effects.custom_preset_tooltip_1": "在上方选择一个预设以应用预定义的气候设置。", + "feature.wetness_effects.debug": "调试", + "feature.wetness_effects.description": "添加逼真的湿润效果,包括基于降雨的表面湿润、积水形成、岸边湿润以及动态雨滴效果,增强天气沉浸感。", + "feature.wetness_effects.effect_range": "效果范围", + "feature.wetness_effects.effect_range_tooltip": "雨滴效果的作用范围", + "feature.wetness_effects.effects": "效果:", + "feature.wetness_effects.enable_interior_exterior_override": "启用室内/室外覆写", + "feature.wetness_effects.enable_puddle_override": "启用积水覆写", + "feature.wetness_effects.enable_rain_override": "启用降雨覆写", + "feature.wetness_effects.enable_raindrop_effects": "启用雨滴效果", + "feature.wetness_effects.enable_ripples": "启用涟漪", + "feature.wetness_effects.enable_ripples_tooltip": "在积水上启用圆形涟漪,在较小程度上也在其他湿润表面上生效", + "feature.wetness_effects.enable_splashes": "启用飞溅", + "feature.wetness_effects.enable_splashes_tooltip": "在干燥表面上启用小型湿润飞溅效果。", + "feature.wetness_effects.enable_vanilla_ripples": "启用原版涟漪", + "feature.wetness_effects.enable_vanilla_ripples_controlled": "启用原版涟漪 - 由Splashes of Storms控制", + "feature.wetness_effects.enable_wetness": "启用湿润效果", + "feature.wetness_effects.enable_wetness_override": "启用湿润覆写", + "feature.wetness_effects.enable_wetness_tooltip": "在水边和下雨时启用表面湿润效果。", + "feature.wetness_effects.grid_size": "网格尺寸", + "feature.wetness_effects.grid_size_tooltip_0": "雨滴放置的空间网格尺寸(越小=更多网格单元,更高的GPU开销)", + "feature.wetness_effects.grid_size_tooltip_1": "这是对性能最敏感的选项。仅在需要更逼真效果时才降低此值。", + "feature.wetness_effects.interior_exterior_override_tooltip": "如果禁用,将仅使用室外值。", + "feature.wetness_effects.interval": "间隔", + "feature.wetness_effects.interval_tooltip": "检查雨滴效果的频率(越低越频繁,中等性能影响)", + "feature.wetness_effects.key_feature_1": "基于天气条件的动态表面湿润", + "feature.wetness_effects.key_feature_2": "逼真的积水形成与岸边湿润效果", + "feature.wetness_effects.key_feature_3": "带动画飞溅和涟漪的雨滴效果", + "feature.wetness_effects.key_feature_4": "可配置的湿润强度和天气转换速度", + "feature.wetness_effects.key_feature_5": "支持皮肤湿润和特定材质响应", + "feature.wetness_effects.lifetime": "生命周期", + "feature.wetness_effects.max_radius": "最大半径", + "feature.wetness_effects.meters_format": "{:.2f} 米", + "feature.wetness_effects.min_radius": "最小半径", + "feature.wetness_effects.min_rain_wetness": "最小降雨湿润度", + "feature.wetness_effects.min_rain_wetness_tooltip": "物体因雨水变湿的最小程度。", + "feature.wetness_effects.name": "湿润效果", + "feature.wetness_effects.open_weather_picker": "打开天气选择器", + "feature.wetness_effects.open_weather_picker_tooltip": "在 CS 实用工具中打开天气选择器", + "feature.wetness_effects.portion_of_grid_size": "作为网格尺寸的比例。", + "feature.wetness_effects.puddle_max_angle": "积水最大角度", + "feature.wetness_effects.puddle_max_angle_tooltip": "表面需要多平才能形成积水。", + "feature.wetness_effects.puddle_min_wetness": "积水最小湿润度", + "feature.wetness_effects.puddle_min_wetness_tooltip": "积水开始形成时的湿润度值。", + "feature.wetness_effects.puddle_radius": "积水半径", + "feature.wetness_effects.puddle_radius_tooltip": "用于确定积水大小和位置的半径", + "feature.wetness_effects.puddle_wetness": "积水湿润度", + "feature.wetness_effects.puddle_wetness_in_exterior": "积水湿润度 室内/室外", + "feature.wetness_effects.radius": "半径", + "feature.wetness_effects.rain_in_exterior": "降雨 室内/室外", + "feature.wetness_effects.rain_system_state": "雨水系统状态", + "feature.wetness_effects.rain_wetness": "降雨湿润度", + "feature.wetness_effects.raindrop_effects": "雨滴效果", + "feature.wetness_effects.raindrops": "雨滴", + "feature.wetness_effects.raindrops_help": "在每个间隔内,每个网格单元中放置一个雨滴。\n只有设定比例的雨滴会实际触发飞溅和涟漪。\n", + "feature.wetness_effects.ripples": "涟漪", + "feature.wetness_effects.shore_range": "岸边范围", + "feature.wetness_effects.shore_range_tooltip": "岸边湿润效果影响水体的最大距离", + "feature.wetness_effects.shore_wetness": "岸边湿润度", + "feature.wetness_effects.skin_wetness": "皮肤湿润度", + "feature.wetness_effects.skin_wetness_tooltip": "雨天时角色皮肤和头发的湿润程度。", + "feature.wetness_effects.splashes": "飞溅", + "feature.wetness_effects.strength": "强度", + "feature.wetness_effects.vanilla_ripples_tooltip_0": "启用默认涟漪(例如Ripples01)。", + "feature.wetness_effects.vanilla_ripples_tooltip_1": "禁用可能要到下次天气变化时才生效。", + "feature.wetness_effects.weather_transition_speed": "天气转换速度", + "feature.wetness_effects.weather_transition_speed_tooltip": "下雨时湿润效果出现的速度以及雨停后干燥的速度。", + "feature.wetness_effects.wetness_effects": "湿润效果", + "feature.wetness_effects.wetness_in_exterior": "湿润度 室内/室外", + "menu.advanced.active_shaders": "活跃着色器", + "menu.advanced.active_shaders_tooltip": "最近帧中使用过的着色器列表。在上方启用着色器拦截可使用热键循环浏览并拦截着色器进行调试。约1秒未使用的着色器将从此列表中移除。", + "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", + "menu.advanced.addresses": "地址", + "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", + "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", + "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", + "menu.advanced.avoid_flow_control": "避免流控制", + "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.background_compiler_threads": "后台编译器线程", + "menu.advanced.background_compiler_threads_tooltip": "游戏过程中用于编译着色器的线程数。默认为性能核心的一半,以避免影响渲染线程。较高值可更快完成编译,但可能导致卡顿。", + "menu.advanced.block_next": "拦截下一个:", + "menu.advanced.block_previous": "拦截上一个:", + "menu.advanced.blocked_shader": "已拦截:%s", + "menu.advanced.change_shader_block_next": "更改##ShaderBlockNext", + "menu.advanced.change_shader_block_prev": "更改##ShaderBlockPrev", + "menu.advanced.clear_shader_cache": "清除着色器缓存", + "menu.advanced.clear_shader_cache_tooltip": "从内存中清除所有已编译的着色器。下次使用时强制重新编译所有着色器。", + "menu.advanced.click_to_block": "左键点击拦截此着色器", + "menu.advanced.click_to_unblock": "左键点击取消拦截此着色器", + "menu.advanced.column_class": "类别", + "menu.advanced.column_class_tooltip": "着色器类别", + "menu.advanced.column_descriptor": "描述符", + "menu.advanced.column_descriptor_tooltip": "着色器描述符", + "menu.advanced.column_frame_pct": "帧百分比", + "menu.advanced.column_frame_pct_tooltip": "此帧中绘制调用的百分比", + "menu.advanced.column_key": "键", + "menu.advanced.column_key_tooltip": "着色器键", + "menu.advanced.column_type": "类型", + "menu.advanced.column_type_tooltip": "着色器类型", + "menu.advanced.compiler_threads": "编译器线程", + "menu.advanced.compiler_threads_tooltip": "启动时用于编译着色器的线程数。默认为所有逻辑核心减去一个以留出系统开销(包含E核)。较高值可更快完成编译,但可能降低系统响应性。", + "menu.advanced.compute": "计算", + "menu.advanced.compute_tooltip": "替换计算着色器。设为false时将禁用上述类型的自定义计算着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.copy_info": "复制信息", + "menu.advanced.copy_info_tooltip": "将包含缓存路径的完整着色器信息复制到剪贴板", + "menu.advanced.copy_key": "复制键", + "menu.advanced.dump_ini_settings": "导出INI设置", + "menu.advanced.dump_shaders": "导出着色器", + "menu.advanced.dump_shaders_tooltip": "在启动时导出着色器。仅在逆向着色器时使用。普通用户无需此功能。", + "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", + "menu.advanced.enable_file_watcher": "启用文件监视器", + "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", + "menu.advanced.enable_shader_blocking": "启用着色器拦截", + "menu.advanced.enable_shader_blocking_tooltip": "启用热键以循环浏览并拦截单个着色器,用于调试目的。", + "menu.advanced.frame_annotations": "帧注释", + "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", + "menu.advanced.half_precision": "半精度(部分精度)", + "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.infinite_core_efficiency": "无限核心效率", + "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", + "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", + "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", + "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", + "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", + "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", + "menu.advanced.log_level": "日志级别", + "menu.advanced.log_level_critical": "严重", + "menu.advanced.log_level_debug": "调试", + "menu.advanced.log_level_err": "错误", + "menu.advanced.log_level_info": "信息", + "menu.advanced.log_level_off": "关闭", + "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", + "menu.advanced.log_level_trace": "跟踪", + "menu.advanced.log_level_warn": "警告", + "menu.advanced.makespan_label": "完工时间(T_p)", + "menu.advanced.makespan_metric": "完工时间(T_p):%s", + "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", + "menu.advanced.open_logs": "打开日志", + "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", + "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", + "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", + "menu.advanced.pixel": "像素", + "menu.advanced.pixel_tooltip": "替换像素着色器。设为false时将禁用上述类型的自定义像素着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.press_key_shader_block_next": "按下任意键设置着色器拦截下一个...", + "menu.advanced.press_key_shader_block_prev": "按下任意键设置着色器拦截上一个...", + "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", + "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", + "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", + "menu.advanced.relative_bar_format": "{}({:.1f}%)", + "menu.advanced.relative_durations": "相对持续时间(归一化)", + "menu.advanced.replace_original_shaders": "替换原始着色器", + "menu.advanced.shader_blocking_active": "着色器拦截已激活", + "menu.advanced.shader_class_label": "类别:%s", + "menu.advanced.shader_compiler_stats": "着色器编译器:{}", + "menu.advanced.shader_debug_header": "着色器调试", + "menu.advanced.shader_defines": "着色器定义", + "menu.advanced.shader_defines_tooltip": "着色器编译器的定义。以分号\";\"分隔。用空格清除。更改后需重建着色器。计算着色器需要重启才能重新编译。", + "menu.advanced.shader_descriptor": "描述符:0x%X", + "menu.advanced.shader_row_tooltip": "类型:{}\n类别:{}\n描述符:0x{:X}\n键:{}\n\n{}", + "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", + "menu.advanced.shader_type_label": "类型:%s", + "menu.advanced.span_label": "跨度(S)", + "menu.advanced.span_metric": "跨度(S,最长):%s", + "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", + "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", + "menu.advanced.statistics": "统计", + "menu.advanced.stop_blocking": "停止拦截##Section", + "menu.advanced.tab_developer": "开发者", + "menu.advanced.tab_disable_at_boot": "启动时禁用", + "menu.advanced.tab_logging": "日志记录", + "menu.advanced.tab_shader_debug": "着色器调试", + "menu.advanced.tab_testing": "测试", + "menu.advanced.test_conditions": "测试条件", + "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", + "menu.advanced.vertex": "顶点", + "menu.advanced.vertex_tooltip": "替换顶点着色器。设为false时将禁用上述类型的自定义顶点着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.work_label": "工作量(W)", + "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", + "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", + "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", + "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", + "menu.clear_shader_cache": "清除着色器缓存", + "menu.clear_shader_cache_tooltip": "清除着色器缓存和磁盘缓存(如果启用)。\n着色器缓存是在运行时替换原版着色器的已编译着色器集合。\n磁盘缓存是磁盘上已编译着色器的集合。清除后意味着着色器仅在游戏再次遇到它们时才重新编译。", + "menu.disable_at_boot_desc": "选择要在启动时禁用的功能。这与删除feature.ini文件相同。重新启用需要重启。", + "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", + "menu.faq.a2": "每个功能都可以在左侧边栏菜单中找到。点击任何功能即可访问其设置。大多数功能包含预设和详细的工具提示,帮助您了解每个设置的作用。", + "menu.faq.a3": "功能可能因硬件不兼容、依赖项缺失或与其他模组冲突而无法加载。请查看\"功能问题\"选项卡,了解有关任何有问题的功能的详细信息。", + "menu.faq.a4": "着色器失败通常由混合文件版本引起。请确保所有功能均为最新,并避免混合测试版本或过时版本的文件。请查看\"功能问题\"选项卡和/或Wiki了解更多信息。更新您的功能并移除任何过时的功能。", + "menu.faq.a5": "首先启用性能叠加层来监控您的FPS。考虑禁用屏幕空间GI等占用资源的功能或降低质量设置。\"显示\"选项卡还包含可以提升性能的升频选项。", + "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", + "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", + "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", + "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", + "menu.faq.q1": "什么是Community Shaders?", + "menu.faq.q2": "如何配置功能?", + "menu.faq.q3": "为什么有些功能无法加载?", + "menu.faq.q4": "编译时出现“着色器失败”?", + "menu.faq.q5": "如何提升性能?", + "menu.faq.q6": "Community Shaders与ENB兼容吗?", + "menu.faq.q7": "菜单热键无效!", + "menu.faq.q8": "我想帮助开发Community Shaders。", + "menu.faq.q9": "Community Shaders是开源的吗?", + "menu.faq.title": "常见问题解答", + "menu.features": "功能", + "menu.features.advanced": "高级", + "menu.features.also_feature": "另见:%s", + "menu.features.apply_override": "应用覆盖", + "menu.features.available_after_restart": "此功能将在重启后可用。", + "menu.features.boot_toggle_tooltip": "切换启动时加载功能。\n当前状态:%s\n需要重启才能使更改生效。\n禁用可消除性能影响。", + "menu.features.cannot_apply_overrides_scene": "在场景特定设置激活时无法应用覆盖。\n请先暂停此功能的场景设置。", + "menu.features.click_to_navigate": "点击导航到%s", + "menu.features.col_constrained_by": "受限于", + "menu.features.col_forced_to": "强制为", + "menu.features.col_impacted_feature": "受影响的功能", + "menu.features.col_setting": "设置", + "menu.features.constraints_explanation": "这些设置在其各自的功能菜单中因约束激活而被禁用。调整约束功能以移除它们。", + "menu.features.disabled": "禁用", + "menu.features.display": "显示", + "menu.features.dont_show_warning": "不再显示此警告", + "menu.features.download_link": "点击此处下载此功能({})", + "menu.features.download_tooltip": "从模组页面下载功能。", + "menu.features.enable_to_access_config": "启用在上述功能以访问其配置选项。", + "menu.features.enabled": "启用", + "menu.features.error_header": "错误", + "menu.features.feature_issues": "功能问题", + "menu.features.features": "功能", + "menu.features.general": "通用", + "menu.features.home": "主页", + "menu.features.no_settings_available": "此功能没有可用设置。", + "menu.features.ok_button": "确定", + "menu.features.pause_weather_overrides": "暂停天气覆盖", + "menu.features.pause_weather_tooltip": "临时禁用此功能的基于天气的设置调整。\n此状态不会被保存。", + "menu.features.profiling": "性能分析", + "menu.features.restore_defaults_tooltip": "恢复此功能的默认设置", + "menu.features.restore_override_tooltip": "从模组文件恢复原始覆盖设置。\n这将丢弃您的自定义设置并恢复为模组作者的推荐设置。", + "menu.features.scene_specific_settings": "场景特定设置", + "menu.features.select_feature_left": "请从左侧选择一个功能。", + "menu.features.select_item_left": "请从左侧选择一个项目。", + "menu.features.settings_adjusted_warning": "由于功能不兼容,您的部分设置已被自动调整。", + "menu.features.settings_hidden_disabled": "功能设置已隐藏,因为此功能在启动时被禁用。", + "menu.features.unloaded_features": "已卸载的功能", + "menu.footer.d3d12_swap_chain": "D3D12 交换链:{status}", + "menu.footer.game_version": "游戏版本:{runtime} {version}", + "menu.footer.gpu": "GPU:{name}", + "menu.home.active_constraints": "活跃设置约束", + "menu.home.click_to_navigate": "点击导航到{feature}", + "menu.home.consider_disabling_at_boot": "考虑在启动时禁用。", + "menu.home.constraint_header_constrained_by": "受限于", + "menu.home.constraint_header_forced_to": "强制为", + "menu.home.constraint_header_setting": "设置", + "menu.home.constraints_desc": "某些设置受其他功能约束。悬停在行上查看详情。", + "menu.home.dev_wiki": "开发者Wiki", + "menu.home.github": "GitHub", + "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", + "menu.home.join_discord": "加入我们的Discord", + "menu.home.nexus_mods": "Nexus Mods", + "menu.home.quick_links": "快速链接", + "menu.home.welcome": "欢迎使用Community Shaders {version}", + "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", + "menu.home.wiki": "Wiki", + "menu.issues.all_ini_loading": "所有功能INI文件加载成功。", + "menu.issues.cancel": "取消", + "menu.issues.cannot_be_undone": "此操作无法撤销!", + "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", + "menu.issues.cleanup_actions": "清理操作:", + "menu.issues.clear_issue_list": "清除问题列表", + "menu.issues.clear_issue_list_tooltip": "清除此问题列表(清理后有用)。", + "menu.issues.compilation_breaking_desc": "以下功能修改了核心着色器文件,必须通过模组管理器完全卸载。如果核心着色器被修改,仅删除INI文件不会修复编译错误。", + "menu.issues.compilation_breaking_header": "破坏编译的功能", + "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", + "menu.issues.core_feature_installed": "核心功能已安装", + "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", + "menu.issues.current_version": "当前版本:%s", + "menu.issues.delete": "删除", + "menu.issues.delete_confirm": "确定要删除功能'%s'的所有文件吗?", + "menu.issues.delete_files_tooltip": "删除与此功能关联的所有文件(INI、着色器等)", + "menu.issues.delete_unknown_tooltip": "删除此未知功能的文件。警告:如果此功能修改了核心着色器,删除可能无法修复编译问题。", + "menu.issues.download_tooltip": "下载 {name}", + "menu.issues.download_version_tooltip": "下载 {name} {version} 或更高版本", + "menu.issues.file_label": "文件:%s", + "menu.issues.files_label": "文件:", + "menu.issues.general_actions": "常规操作:", + "menu.issues.guidance_label": "指导:%s", + "menu.issues.hlsl_files_count": "%zu个HLSL文件", + "menu.issues.hlsl_files_found": "HLSL文件:找到%zu个", + "menu.issues.ini_file_label": "INI文件:%s", + "menu.issues.ini_label": "INI:%s", + "menu.issues.ini_path": "INI路径:%s", + "menu.issues.issue_label": "问题:%s", + "menu.issues.last_modified": "最后修改:", + "menu.issues.minimum_required": "最低要求:%s", + "menu.issues.no_issues": "未发现功能问题!", + "menu.issues.obsolete_compilation_failure": "此过时功能修改了核心着色器文件并导致编译失败。必须通过模组管理器卸载。", + "menu.issues.obsolete_features_desc": "以下功能已过时并已自动禁用。这些功能在此CS版本中已被移除或替换,但未修改核心着色器。", + "menu.issues.obsolete_features_header": "过时功能", + "menu.issues.open_features_folder": "打开功能文件夹", + "menu.issues.open_features_folder_tooltip": "打开包含INI文件的功能文件夹以供手动审查。", + "menu.issues.open_logs": "打开日志", + "menu.issues.open_logs_tooltip": "打开CommunityShaders.log文件以供手动审查。", + "menu.issues.open_shaders_directory": "打开着色器目录", + "menu.issues.open_shaders_tooltip": "打开主着色器目录以查看各个功能着色器文件夹。", + "menu.issues.override_failures_desc": "以下覆盖文件加载或应用失败。请检查文件格式和内容。", + "menu.issues.override_failures_header": "覆盖失败", + "menu.issues.potential_compilation_failure": "潜在的编译失败", + "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", + "menu.issues.replaced_by_prefix": "(被替换为", + "menu.issues.replaced_by_suffix": ")", + "menu.issues.replacement_label": "替代:%s", + "menu.issues.shader_directory_label": "着色器目录:%s", + "menu.issues.shader_folder": "着色器文件夹:%s", + "menu.issues.test.active_inis_count": "活动的测试 INI 文件({count}):\n", + "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", + "menu.issues.test.create_test_inis": "创建测试INI", + "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", + "menu.issues.test.feature_issue_testing": "功能问题测试", + "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", + "menu.issues.test.modified_notice": "\n部分测试文件已修改 - 建议恢复以清理", + "menu.issues.test.no_active_inis": "当前没有活跃的测试INI文件。", + "menu.issues.test.restore": "恢复", + "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", + "menu.issues.test.testing_header": "测试", + "menu.issues.this_will_delete": "这将删除:", + "menu.issues.time_label": "时间:%s", + "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", + "menu.issues.unknown_compilation_warning": "此未知功能可能修改了核心着色器文件,并可能导致编译失败。如果故障继续,应移除未知功能。", + "menu.issues.unknown_delete_warning": "这是一个未知功能。如果它修改了核心着色器文件(在其自身文件夹之外),仅删除这些文件不会修复着色器编译问题。", + "menu.issues.unknown_features_desc": "以下功能未被识别,我们已尝试自动禁用。它们可能来自开发分支或较新的CS版本。由于我们无法确定它们可能修改了哪些文件,应作为预防措施将其移除,以防止潜在的着色器编译失败。", + "menu.issues.unknown_features_header": "未知功能", + "menu.issues.update_no_link_tooltip": "此功能需要更新,但没有可用的下载链接。请手动检查模组页面。", + "menu.issues.update_required": "需要更新", + "menu.issues.update_to_version_required": "需要更新到 {version}+", + "menu.issues.use_clear_issue_list": "手动清理后使用\"清除问题列表\"刷新", + "menu.issues.use_open_features_folder": "使用\"打开功能文件夹\"手动审查INI文件", + "menu.issues.use_open_logs": "使用\"打开日志\"手动审查日志", + "menu.issues.use_open_shaders_directory": "使用\"打开着色器目录\"检查孤立着色器文件夹", + "menu.issues.warning_label": "警告:", + "menu.issues.wrong_version_desc": "以下功能存在版本兼容性问题,已自动禁用。请检查是否有更新或者该功能是否被视为过时。", + "menu.issues.wrong_version_header": "错误版本功能", + "menu.restore_settings": "恢复已保存的设置", + "menu.save_settings": "保存设置", + "menu.settings.auto_hide_feature_list": "自动隐藏功能列表", + "menu.settings.auto_hide_feature_list_tooltip": "自动隐藏左侧功能列表面板。将光标移到左边缘即可显示。", + "menu.settings.background_blur": "背景模糊", + "menu.settings.background_blur_tooltip": "对菜单窗口后面的背景应用模糊效果。", + "menu.settings.base_font_size": "基础字体大小", + "menu.settings.borders_and_separators": "边框和分隔符", + "menu.settings.button_text_align": "按钮文本对齐", + "menu.settings.button_text_align_tooltip": "当按钮大于其文本内容时应用对齐。", + "menu.settings.cancel": "取消", + "menu.settings.cell_padding": "单元格内边距", + "menu.settings.center_header_title": "居中标题", + "menu.settings.center_header_title_tooltip": "将Community Shaders标题和徽标在标题栏中居中", + "menu.settings.child_border_size": "子窗口边框大小", + "menu.settings.child_rounding": "子窗口圆角", + "menu.settings.color_background": "背景", + "menu.settings.color_border": "边框", + "menu.settings.color_border_shadow": "边框阴影", + "menu.settings.color_button": "按钮", + "menu.settings.color_button_active": "按钮(激活)", + "menu.settings.color_button_hovered": "按钮(悬停)", + "menu.settings.color_button_left": "左侧", + "menu.settings.color_button_position": "颜色按钮位置", + "menu.settings.color_button_right": "右侧", + "menu.settings.color_check_mark": "复选框勾选标记", + "menu.settings.color_child_bg": "子窗口背景", + "menu.settings.color_current_hotkey": "当前热键", + "menu.settings.color_default": "默认", + "menu.settings.color_disabled": "禁用", + "menu.settings.color_docking_empty_bg": "停靠空白背景", + "menu.settings.color_docking_preview": "停靠预览", + "menu.settings.color_drag_drop_target": "拖放目标", + "menu.settings.color_drag_drop_target_bg": "拖放目标背景", + "menu.settings.color_error": "错误", + "menu.settings.color_frame_bg": "框架背景", + "menu.settings.color_frame_bg_active": "框架背景(激活)", + "menu.settings.color_frame_bg_hovered": "框架背景(悬停)", + "menu.settings.color_header": "标题", + "menu.settings.color_header_active": "标题(激活)", + "menu.settings.color_header_hovered": "标题(悬停)", + "menu.settings.color_hovered": "悬停", + "menu.settings.color_info": "信息", + "menu.settings.color_input_text_cursor": "输入文本光标", + "menu.settings.color_menu_bar_bg": "菜单栏背景", + "menu.settings.color_minimized_transparency": "最小化透明度", + "menu.settings.color_modal_window_dim_bg": "模态窗模糊背景", + "menu.settings.color_nav_cursor": "导航光标", + "menu.settings.color_nav_windowing_dim_bg": "窗口导航模糊背景", + "menu.settings.color_nav_windowing_highlight": "窗口导航高亮", + "menu.settings.color_plot_histogram": "图表直方图", + "menu.settings.color_plot_histogram_hovered": "图表直方图(悬停)", + "menu.settings.color_plot_lines": "图表折线", + "menu.settings.color_plot_lines_hovered": "图表折线(悬停)", + "menu.settings.color_popup_bg": "弹出窗口背景", + "menu.settings.color_resize_grip": "调整大小手柄", + "menu.settings.color_resize_grip_active": "调整大小手柄(激活)", + "menu.settings.color_resize_grip_hovered": "调整大小手柄(悬停)", + "menu.settings.color_restart_needed": "需要重启", + "menu.settings.color_scrollbar_bg": "滚动条背景", + "menu.settings.color_scrollbar_grab": "滚动条滑块", + "menu.settings.color_scrollbar_grab_active": "滚动条滑块(激活)", + "menu.settings.color_scrollbar_grab_hovered": "滚动条滑块(悬停)", + "menu.settings.color_separator": "分隔符", + "menu.settings.color_separator_active": "分隔符(激活)", + "menu.settings.color_separator_hovered": "分隔符(悬停)", + "menu.settings.color_separator_line": "分隔线", + "menu.settings.color_slider_grab": "滑块手柄", + "menu.settings.color_slider_grab_active": "滑块手柄(激活)", + "menu.settings.color_slider_input_bg": "滑块和输入框背景", + "menu.settings.color_success": "成功", + "menu.settings.color_tab": "标签页", + "menu.settings.color_tab_dimmed": "标签页(变暗)", + "menu.settings.color_tab_dimmed_selected": "标签页(变暗选中)", + "menu.settings.color_tab_dimmed_selected_overline": "标签页变暗选中上划线", + "menu.settings.color_tab_hovered": "标签页(悬停)", + "menu.settings.color_tab_selected": "标签页(选中)", + "menu.settings.color_tab_selected_overline": "标签页选中上划线", + "menu.settings.color_table_border_light": "表格边框(浅色)", + "menu.settings.color_table_border_strong": "表格边框(深色)", + "menu.settings.color_table_header_bg": "表格标题背景", + "menu.settings.color_table_row_bg": "表格行背景", + "menu.settings.color_table_row_bg_alt": "表格行背景(交替)", + "menu.settings.color_text": "文本", + "menu.settings.color_text_disabled": "文本(禁用)", + "menu.settings.color_text_link": "文本链接", + "menu.settings.color_text_selected_bg": "文本选择背景", + "menu.settings.color_title_bg": "标题栏背景", + "menu.settings.color_title_bg_active": "标题栏背景(激活)", + "menu.settings.color_title_bg_collapsed": "标题栏背景(折叠)", + "menu.settings.color_tree_lines": "树状线条", + "menu.settings.color_unsaved_marker": "未保存标记", + "menu.settings.color_warning": "警告", + "menu.settings.color_window_bg": "窗口背景", + "menu.settings.color_window_border": "窗口边框", + "menu.settings.create_new_theme": "创建新主题", + "menu.settings.create_new_theme_hint": "使用当前设置创建新主题:", + "menu.settings.create_theme": "创建主题", + "menu.settings.cs_editor_toggle_key": "CS 编辑器切换键:", + "menu.settings.delete_button": "删除", + "menu.settings.delete_theme": "删除主题", + "menu.settings.delete_theme_confirm_part1": "您确定要删除主题'", + "menu.settings.delete_theme_confirm_part2": "'?\n\n这将永久删除主题文件。此操作不可撤销。", + "menu.settings.delete_theme_title": "删除主题", + "menu.settings.delete_theme_tooltip": "删除'%s'的主题文件。此操作不可撤销。", + "menu.settings.description": "描述", + "menu.settings.description_tooltip": "主题的可选描述", + "menu.settings.display_name": "显示名称", + "menu.settings.display_name_duplicate": "已存在具有此显示名称的主题", + "menu.settings.display_name_tooltip": "在下拉菜单中显示的人类可读名称", + "menu.settings.docking_splitter_size": "停靠分隔器大小", + "menu.settings.effect_toggle_key": "效果切换键:", + "menu.settings.effective_size": "有效大小:%.0f px", + "menu.settings.enable_async": "启用异步", + "menu.settings.enable_async_tooltip": "如果着色器尚未编译则跳过替换。还会使编译速度极快!", + "menu.settings.enable_disk_cache": "启用磁盘缓存", + "menu.settings.enable_disk_cache_tooltip": "禁用从磁盘加载着色器,并阻止将已编译着色器保存到磁盘缓存。", + "menu.settings.feature_header_scale": "功能标题缩放", + "menu.settings.feature_header_scale_tooltip": "设置选项卡中功能标题文本的缩放倍率。", + "menu.settings.feature_headings": "功能标题", + "menu.settings.file_label": "文件:%s", + "menu.settings.filter_colors": "过滤颜色", + "menu.settings.font": "字体", + "menu.settings.font_roles": "字体角色", + "menu.settings.frame_border_size": "框架边框大小", + "menu.settings.frame_padding": "框架内边距", + "menu.settings.frame_rounding": "框架圆角", + "menu.settings.full_palette": "完整调色板", + "menu.settings.full_palette_tooltip": "用于详细自定义所有UI元素的高级颜色控制。", + "menu.settings.global_scale": "全局缩放", + "menu.settings.grab_min_size": "滑块最小大小", + "menu.settings.grab_rounding": "滑块圆角", + "menu.settings.indent_spacing": "缩进间距", + "menu.settings.item_inner_spacing": "项目内部间距", + "menu.settings.item_spacing": "项目间距", + "menu.settings.language": "语言", + "menu.settings.language_tooltip": "选择Community Shaders界面的显示语言。", + "menu.settings.last_shader_cache_duration": "上次着色器缓存构建持续时间:%s", + "menu.settings.log_slider_deadzone": "对数滑块死区", + "menu.settings.no_families": "无字体家族", + "menu.settings.no_font_families_available": "无可用字体家族", + "menu.settings.no_fonts_found": "未找到字体。请将.ttf文件放入Interface/CommunityShaders/Fonts/", + "menu.settings.no_style_variants": "未找到此字体家族的样式变体。", + "menu.settings.no_styles": "无样式", + "menu.settings.open_themes_folder": "打开主题文件夹", + "menu.settings.open_themes_folder_tooltip": "打开主题文件夹,您可以在其中添加自定义主题文件。", + "menu.settings.overlay_toggle_key": "叠加层切换键:", + "menu.settings.popup_border_size": "弹出窗口边框大小", + "menu.settings.popup_rounding": "弹出窗口圆角", + "menu.settings.refresh": "刷新", + "menu.settings.refresh_font_families": "刷新字体家族", + "menu.settings.refresh_font_families_tooltip": "添加或删除字体文件后重新扫描字体目录。", + "menu.settings.require_shift_to_dock": "需要Shift键停靠", + "menu.settings.require_shift_to_dock_tooltip": "启用时,拖动时必须按住Shift键才能停靠/对齐窗口。防止意外停靠。", + "menu.settings.reset": "重置", + "menu.settings.save_as_new_theme": "保存为新主题", + "menu.settings.save_theme_button": "保存", + "menu.settings.save_theme_tooltip": "使用当前设置更新当前选中的主题(%s)", + "menu.settings.screenshot_key": "截图键:", + "menu.settings.scrollbar_opacity": "滚动条不透明度", + "menu.settings.scrollbar_rounding": "滚动条圆角", + "menu.settings.scrollbar_size": "滚动条大小", + "menu.settings.section_borders": "边框", + "menu.settings.section_docking": "停靠", + "menu.settings.section_language": "语言", + "menu.settings.section_layout": "布局", + "menu.settings.section_main": "主页", + "menu.settings.section_rounding": "圆角", + "menu.settings.section_tables": "表格", + "menu.settings.section_widgets": "控件", + "menu.settings.selectable_text_align": "可选取文本对齐", + "menu.settings.selectable_text_align_tooltip": "当可选取项大于其文本内容时应用对齐。", + "menu.settings.selected_theme": "已选主题:", + "menu.settings.separator_text_align": "分隔符文本对齐", + "menu.settings.separator_text_border_size": "分隔符文本边框大小", + "menu.settings.separator_text_padding": "分隔符文本内边距", + "menu.settings.shader_deduplicated": "已去重", + "menu.settings.shader_disk_cache": "磁盘缓存", + "menu.settings.shader_failed": "失败", + "menu.settings.shader_fast": "快速(<2秒)", + "menu.settings.shader_slow": "慢速(2-8秒)", + "menu.settings.shader_very_slow": "非常慢(>=8秒)", + "menu.settings.show_footer": "显示页脚", + "menu.settings.show_footer_tooltip": "在窗口底部显示包含游戏版本、交换链和GPU信息的页脚", + "menu.settings.show_icon_buttons_in_header": "在标题栏中显示图标按钮", + "menu.settings.show_icon_buttons_in_header_tooltip": "启用时:在标题栏中将操作按钮(保存、加载、清除缓存)显示为图标\n禁用时:在标题栏下方显示为文本按钮", + "menu.settings.skip_clear_cache_dialogue": "跳过清除缓存对话框", + "menu.settings.skip_clear_cache_dialogue_tooltip": "勾选时,着色器缓存将立即清除,无需确认。", + "menu.settings.skip_compilation_key": "跳过编译键:", + "menu.settings.skip_unchanged_shaders": "跳过未更改的着色器", + "menu.settings.skip_unchanged_shaders_tooltip": "启用时,仅当每个着色器的.hlsl文件比磁盘上缓存的.bin更新时才从源代码重新编译。源文件未更改的着色器直接从磁盘缓存加载,避免完整的启动编译开销。适用于迭代测试:更改着色器文件后仅重建该着色器。需要\"启用磁盘缓存\"处于活动状态。", + "menu.settings.status": "状态", + "menu.settings.tab_bar_border_size": "标签栏边框大小", + "menu.settings.tab_behavior": "行为", + "menu.settings.tab_border_size": "标签页边框大小", + "menu.settings.tab_colors": "颜色", + "menu.settings.tab_fonts": "字体", + "menu.settings.tab_interface": "界面", + "menu.settings.tab_keybindings": "按键绑定", + "menu.settings.tab_rounding": "标签页圆角", + "menu.settings.tab_shaders": "着色器", + "menu.settings.tab_styling": "样式", + "menu.settings.tab_themes": "主题", + "menu.settings.table_angled_headers_angle": "表格斜角标题角度", + "menu.settings.theme_name": "主题名称", + "menu.settings.theme_name_duplicate": "已存在具有此名称的主题", + "menu.settings.theme_name_required": "主题名称为必填", + "menu.settings.theme_name_tooltip": "主题的文件名(不含.json扩展名)", + "menu.settings.theme_preset": "主题预设", + "menu.settings.theme_save_info": "主题更改不会随全局\"保存设置\"按钮保存。使用主题选项卡将更改保存到此主题。", + "menu.settings.theme_save_reminder": "如果您更改了上述主题,请使用全局\"保存设置\"按钮保存您的选择。", + "menu.settings.theme_update_failed": "更新主题失败", + "menu.settings.theme_updated_no_changes": "主题更新成功 - 未检测到更改", + "menu.settings.theme_updated_with_changes": "主题更新成功!更改的设置:", + "menu.settings.thumb_active_opacity": "滑块激活不透明度", + "menu.settings.thumb_active_opacity_tooltip": "控制滚动条滑块被拖动时的不透明度。", + "menu.settings.thumb_hovered_opacity": "滑块悬停不透明度", + "menu.settings.thumb_hovered_opacity_tooltip": "控制滚动条滑块悬停时的不透明度。", + "menu.settings.thumb_opacity": "滑块不透明度", + "menu.settings.thumb_opacity_tooltip": "控制滚动条滑块(可拖动的部分)的不透明度。", + "menu.settings.toggle_key": "切换键:", + "menu.settings.tooltip_hover_delay": "工具提示悬停延迟", + "menu.settings.tooltip_hover_delay_tooltip": "悬停在项目上时工具提示出现前等待的秒数。", + "menu.settings.track_opacity": "滚动轨道不透明度", + "menu.settings.track_opacity_tooltip": "控制滚动条轨道/通道(滚动条后面的背景区域)的不透明度。", + "menu.settings.ui_behavior": "UI行为", + "menu.settings.use_custom_shaders": "使用自定义着色器", + "menu.settings.use_custom_shaders_tooltip": "禁用此项实际上会禁用所有功能。", + "menu.settings.use_monochrome_cs_logo": "使用单色CS徽标", + "menu.settings.use_monochrome_cs_logo_tooltip": "使用Community Shaders徽标的单色版本", + "menu.settings.use_monochrome_icons": "使用单色图标", + "menu.settings.use_monochrome_icons_tooltip": "使用适应主题文本颜色的白色单色图标", + "menu.settings.use_resolution_based_font_size": "使用基于分辨率的字体大小", + "menu.settings.use_resolution_based_font_size_tooltip": "启用时,UI字体大小根据屏幕分辨率缩放。禁用以设置固定大小。", + "menu.settings.visual_effects": "视觉效果", + "menu.settings.window_border_size": "窗口边框大小", + "menu.settings.window_padding": "窗口内边距", + "menu.settings.window_rounding": "窗口圆角", + "menu.setup.change_later": "您可以稍后在通用 > 按键绑定中更改此项。", + "menu.setup.choose_hotkey": "请选择一个热键来访问菜单:", + "menu.setup.cs_editor_unbound": "CS 编辑器热键未绑定 - 所选键使用 Shift", + "menu.setup.cs_editor_will_be": "CS 编辑器热键将为:{key}", + "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", + "menu.setup.new_install_line2": "重新安装Community Shaders。", + "menu.setup.press_any_key": "按下任意键设置为切换键...", + "menu.setup.press_to_close": "按Escape或Enter继续", + "menu.toggle_error_message": "切换错误消息", + "menu.toggle_error_message_tooltip": "隐藏或显示着色器失败消息。您的安装已损坏,游戏中可能会看到错误。请仔细检查是否已更新所有功能以及加载顺序是否正确。请参阅CommunityShaders.log了解详情,并查看Nexus Mods页面或Discord服务器。", + "menu.window_title": "Community Shaders {version}", + "menu.window_title_dev": "Community Shaders {version} [{build}]", + "overlay.modified_features": "检测到可能修改了着色器的功能。请检查菜单中的功能问题。", + "overlay.shader_blocking_active": "着色器拦截已激活", + "overlay.uncompiled_warning": "警告:未编译的着色器在加载时会有视觉错误或导致卡顿。", + "ui.cancel": "取消", + "ui.clear_cache": "清除缓存", + "ui.clear_cache_confirm": "您确定要清除着色器缓存吗?", + "ui.clear_cache_desc": "这将清除内存和磁盘缓存(如果启用)中的所有已编译着色器。着色器将在游戏下次遇到它们时重新编译。", + "ui.clear_shader_cache": "清除着色器缓存?", + "ui.copy": "复制", + "ui.dont_ask_again": "不再提示", + "ui.search": "搜索...", + "ui.search_features": "搜索功能..." } diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index cce48d379a..a232895eda 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -7,7 +7,7 @@ #include "State.h" #include "Util.h" -#define I18N_KEY_PREFIX "feature.ssgi." +#define I18N_KEY_PREFIX "feature.screen_space_gi." NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ScreenSpaceGI::Settings, diff --git a/tools/sort-i18n.py b/tools/sort-i18n.py new file mode 100644 index 0000000000..86c4e1ca0b --- /dev/null +++ b/tools/sort-i18n.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +sort-i18n.py — Ensure non-English translation files follow en.json key order. + +Reads en.json as the reference for key ordering, then checks (or rewrites) +all other translation JSON files so their keys appear in the same order. + +Usage: + python tools/sort-i18n.py # Preview (dry-run) + python tools/sort-i18n.py --write # Rewrite translation files in-place + python tools/sort-i18n.py --check # CI mode: exit 1 if any file is mis-ordered +""" + +import argparse +import json +import sys +from pathlib import Path + + +def find_project_root(): + """Find the project root by looking for CMakeLists.txt.""" + d = Path(__file__).resolve().parent.parent + if (d / "CMakeLists.txt").exists(): + return d + d = Path.cwd() + while d != d.parent: + if (d / "CMakeLists.txt").exists(): + return d + d = d.parent + print("Error: Could not find project root (CMakeLists.txt)", file=sys.stderr) + sys.exit(1) + + +def get_en_key_order(en_path: Path) -> list[str]: + """Load en.json and return the ordered list of translation keys (excluding _meta).""" + with open(en_path, "r", encoding="utf-8") as f: + data = json.load(f) + return [k for k in data if k != "_meta"] + + +def sort_translation_file(data: dict, en_key_order: list[str]) -> dict: + """ + Return a new ordered dict with: + 1. _meta first (if present) + 2. Keys that exist in en.json, in en.json order + 3. Any extra keys not in en.json, sorted alphabetically at the end + """ + en_order_set = set(en_key_order) + sorted_data = {} + + # _meta always first + if "_meta" in data: + sorted_data["_meta"] = data["_meta"] + + # Keys following en.json order + for key in en_key_order: + if key in data: + sorted_data[key] = data[key] + + # Extra keys not in en.json (sorted alphabetically) + extra_keys = sorted(k for k in data if k != "_meta" and k not in en_order_set) + for key in extra_keys: + sorted_data[key] = data[key] + + return sorted_data + + +def check_order(data: dict, en_key_order: list[str]) -> bool: + """Check if the translation file keys are already in the correct order.""" + en_order_set = set(en_key_order) + locale_keys = [k for k in data if k != "_meta"] + + # Build expected order: en.json keys (that exist in this file) + extra keys sorted + expected_keys = [k for k in en_key_order if k in data] + extra_keys = sorted(k for k in data if k != "_meta" and k not in en_order_set) + expected_keys.extend(extra_keys) + + return locale_keys == expected_keys + + +def main(): + # Force UTF-8 output on Windows + if sys.stdout.encoding != "utf-8": + sys.stdout.reconfigure(encoding="utf-8") + if sys.stderr.encoding != "utf-8": + sys.stderr.reconfigure(encoding="utf-8") + + parser = argparse.ArgumentParser( + description="Sort translation file keys to match en.json order." + ) + parser.add_argument("--write", action="store_true", + help="Rewrite translation files with correct key order") + parser.add_argument("--check", action="store_true", + help="CI mode: exit 1 if any translation file has wrong key order") + args = parser.parse_args() + + root = find_project_root() + translations_dir = (root / "package" / "SKSE" / "Plugins" + / "CommunityShaders" / "Translations") + en_path = translations_dir / "en.json" + + if not en_path.exists(): + print("Error: en.json not found", file=sys.stderr) + sys.exit(1) + + en_key_order = get_en_key_order(en_path) + print(f"Reference: en.json ({len(en_key_order)} keys)") + + locale_files = sorted( + p for p in translations_dir.glob("*.json") if p.name != "en.json" + ) + + if not locale_files: + print("No translation files to check.") + sys.exit(0) + + misordered = [] + + for path in locale_files: + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError as e: + print(f" {path.name}: SKIP (invalid JSON: {e})") + continue + + if not isinstance(data, dict): + print(f" {path.name}: SKIP (root is not a JSON object)") + continue + + if check_order(data, en_key_order): + print(f" {path.name}: OK") + else: + misordered.append(path) + print(f" {path.name}: keys are NOT in en.json order") + + if args.write: + sorted_data = sort_translation_file(data, en_key_order) + output_text = json.dumps(sorted_data, indent=4, ensure_ascii=False) + "\n" + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(output_text) + print(f" -> Rewritten with correct key order") + + print() + if misordered: + if args.write: + print(f"Fixed {len(misordered)} file(s).") + elif args.check: + print( + f"{len(misordered)} file(s) have keys not matching en.json order:", + file=sys.stderr + ) + for p in misordered: + print(f" - {p.name}", file=sys.stderr) + print( + "\nRun: python tools/sort-i18n.py --write", + file=sys.stderr + ) + sys.exit(1) + else: + print(f"{len(misordered)} file(s) would be rewritten.") + print("Use --write to fix them, or --check for CI validation.") + else: + print("All translation files are correctly ordered.") + + +if __name__ == "__main__": + main() From 8fa4e90a1fd0e9a455eaa39df5c64fc56f12c71a Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:06:17 +0200 Subject: [PATCH 21/55] chore(tools): add shader-refactor bytecode verifier (#2464) Co-authored-by: Alan Tse Co-authored-by: Claude Opus 4.8 (1M context) --- .claude/CLAUDE.md | 9 ++ docs/development/shader-workflow.md | 22 +++ tools/verify-shader-refactor.ps1 | 208 ++++++++++++++++++++++++++++ tools/verify-shader-refactor.sh | 19 +++ 4 files changed, 258 insertions(+) create mode 100644 tools/verify-shader-refactor.ps1 create mode 100644 tools/verify-shader-refactor.sh diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index cb01ff5f49..814cbac0df 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -87,8 +87,17 @@ hlslkit-generate-defines --log CommunityShaders.log # Scan for buffer conflicts across features hlslkit-buffer-scan --features-dir features/ + +# Prove a shader refactor changed no behavior (compiles base ref vs working tree, +# compares DXBC across VR x HDR_OUTPUT permutations; exit 0 identical / 2 differs) +pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # bash: tools/verify-shader-refactor.sh ``` +When refactoring an existing shader (especially the decompile-transcription shaders like +`ISTemporalAA.hlsl`), use `tools/verify-shader-refactor.ps1` to prove the change is +behavior-preserving: identical compiled bytecode means a provable no-op. See +`docs/development/shader-workflow.md` for details. + ### Custom CMake Targets **Package and Deployment Targets**: diff --git a/docs/development/shader-workflow.md b/docs/development/shader-workflow.md index f32233a73e..bf58da9431 100644 --- a/docs/development/shader-workflow.md +++ b/docs/development/shader-workflow.md @@ -8,8 +8,30 @@ cmake --build build/ALL --target COPY_SHADERS # Full deployment (DLL + tests + shaders) cmake --build build/ALL --target DEPLOY_ALL + +# Prove an HLSL refactor changed no behavior (compares compiled DXBC vs a git ref) +pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # or tools/verify-shader-refactor.sh ``` +## Verifying refactors + +`tools/verify-shader-refactor.ps1` (bash wrapper: `tools/verify-shader-refactor.sh`) +compiles a shader from a base git ref and from the working tree across the +`VR` × `HDR_OUTPUT` permutations, then compares the compiled bytecode. The base +ref's whole include tree is materialized (via `git archive`), so the base compiles +against base-ref `.hlsli` headers and the working tree against working headers — a +refactor that also edits a shared header is compared correctly, not masked: + +- **IDENTICAL** SHA-256 of the `.cso` ⇒ the refactor is a provable no-op (fxc emits + no timestamps without `/Zi`, so identical source ⇒ identical bytes). +- **DIFFERS** ⇒ it dumps and diffs the `/Fc` assembly so a legitimate-but-non-identical + change can be reviewed. + +Exit codes: `0` all identical, `2` some differ, `1` compile error. Defaults to comparing +the working tree against `merge-base(HEAD, origin/dev)`; pass `-BaseRef ` to override. +Requires `fxc.exe` from the Windows SDK. The permutation sweep is strong evidence, not the +full `shader-validation.yaml` matrix — pass `-Permutations` for exotic define combos. + ## Overview Two deployment targets for different workflows: diff --git a/tools/verify-shader-refactor.ps1 b/tools/verify-shader-refactor.ps1 new file mode 100644 index 0000000000..c18954f977 --- /dev/null +++ b/tools/verify-shader-refactor.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + Prove an HLSL refactor is behavior-preserving by comparing compiled bytecode. + +.DESCRIPTION + Compiles a shader from a base git revision and from the current working tree + across a set of preprocessor permutations, then compares the resulting DXBC. + The base ref's entire include tree (-IncludeDir) is materialized via git archive, + so the base compiles against base-ref headers and the working tree against working + headers -- a refactor that also edits a shared .hlsli is therefore compared correctly. + + Tier 1 (this script): identical SHA-256 of the compiled .cso == provably identical + GPU program. fxc emits no timestamps without /Zi, so same source -> same bytes. + + Tier 2 (this script, on mismatch): dumps /Fc assembly for both revisions and lists + the differing lines (base/work markers), so a legitimate-but-non-identical refactor + (e.g. register reorder) can be eyeballed. + + A refactor that is Tier-1 IDENTICAL on the swept permutations needs no further proof. + Note: the default sweep (VR x HDR_OUTPUT) is strong evidence, not the full build + matrix from shader-validation.yaml. Pass -Permutations for exotic define combos. + +.PARAMETER Shader + Path to the .hlsl file (repo-relative or absolute). + +.PARAMETER BaseRef + Git ref to treat as "before". Default: merge-base of HEAD and origin/dev. + +.PARAMETER IncludeDir + Shader include root passed to fxc /I. Default: package/Shaders. + +.PARAMETER Permutations + Optional explicit permutation list; each entry is a space-separated define set, + e.g. -Permutations "PSHADER","PSHADER VR". Overrides the auto sweep. + +.PARAMETER Entry + Shader entry point. Default: main. + +.PARAMETER Profile + fxc target profile. Default: auto (cs_5_0 for *CS.hlsl, else ps_5_0). + +.EXAMPLE + pwsh tools/verify-shader-refactor.ps1 package/Shaders/ISTemporalAA.hlsl + +.EXAMPLE + pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl -BaseRef HEAD~1 +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Shader, + [string]$BaseRef, + [string]$IncludeDir = "package/Shaders", + [string[]]$Permutations, + [string]$Entry = "main", + [string]$Profile, + [string]$Fxc +) + +# Continue (not Stop): native git calls write warnings to stderr that would otherwise +# abort the run; control flow keys off explicit $LASTEXITCODE checks and `throw`. +$ErrorActionPreference = "Continue" + +function Resolve-Fxc { + if ($Fxc -and (Test-Path $Fxc)) { return $Fxc } + $cmd = Get-Command fxc.exe -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + $roots = @("${env:ProgramFiles(x86)}\Windows Kits\10\bin", "${env:ProgramFiles}\Windows Kits\10\bin") + $found = foreach ($r in $roots) { + if (Test-Path $r) { + Get-ChildItem -Path $r -Recurse -Filter fxc.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "x64" } + } + } + $pick = $found | Sort-Object FullName -Descending | Select-Object -First 1 + if (-not $pick) { throw "fxc.exe not found. Install the Windows 10/11 SDK or pass -Fxc." } + return $pick.FullName +} + +# Resolve repo root so the script works from any cwd. +$repoRoot = (git rev-parse --show-toplevel 2>$null) +if (-not $repoRoot) { throw "Not inside a git repository." } +Push-Location $repoRoot +$work = $null +try { + $fxcPath = Resolve-Fxc + + # Normalize the shader path to repo-relative (forward slashes) for git. + # (Path.GetRelativePath is unavailable in Windows PowerShell 5.1 / .NET Framework.) + $shaderFull = (Resolve-Path $Shader).Path + $rootFull = (Resolve-Path $repoRoot).Path + if (-not $shaderFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) { + throw "Shader '$Shader' resolves outside the repo root '$rootFull'." + } + $relPath = $shaderFull.Substring($rootFull.Length).TrimStart('\', '/').Replace('\', '/') + + if (-not $BaseRef) { + $BaseRef = (git merge-base HEAD origin/dev 2>$null) + if (-not $BaseRef) { $BaseRef = "HEAD" } + } + + if (-not $Profile) { + $Profile = if ($relPath -match 'CS\.hlsl$') { "cs_5_0" } else { "ps_5_0" } + } + $stageDefine = switch -Wildcard ($Profile) { + "cs_*" { "CSHADER" } + "vs_*" { "VSHADER" } + default { "PSHADER" } + } + + if (-not $Permutations -or $Permutations.Count -eq 0) { + $Permutations = @( + "$stageDefine", + "$stageDefine VR", + "$stageDefine HDR_OUTPUT", + "$stageDefine VR HDR_OUTPUT" + ) + } + + # Materialize the base revision's FULL include tree (not just the target shader) so a + # refactor that also touches a shared .hlsli is compared correctly: base compiles against + # base-ref headers, work compiles against working-tree headers. git archive -> tar (via a + # file, never a PS pipeline, which would corrupt the binary tar). + $work = Join-Path ([IO.Path]::GetTempPath()) ("shaderverify_" + [Guid]::NewGuid().ToString("N")) + $baseRoot = Join-Path $work "base" + New-Item -ItemType Directory -Force $baseRoot | Out-Null + $tar = Join-Path $work "base.tar" + git archive --format=tar -o $tar $BaseRef -- $IncludeDir $relPath 2>$null + if ($LASTEXITCODE -ne 0 -or -not (Test-Path $tar)) { + throw "git archive failed for '$BaseRef' (paths: $IncludeDir, $relPath)." + } + tar -xf $tar -C $baseRoot + if ($LASTEXITCODE -ne 0) { throw "Failed to extract base archive." } + $baseFile = Join-Path $baseRoot $relPath + $baseInclude = Join-Path $baseRoot $IncludeDir + if (-not (Test-Path $baseFile)) { throw "'$relPath' not found at '$BaseRef'." } + + function Compile([string]$src, [string]$incDir, [string]$defs, [string]$outFile, [switch]$Asm) { + # Preserve explicit-valued defines (e.g. SHADOWFILTER=0); only bare names get =1. + $defArgs = @() + foreach ($d in ($defs -split '\s+' | Where-Object { $_ })) { + $defArgs += "/D" + $defArgs += $(if ($d -like '*=*') { $d } else { "$d=1" }) + } + $fmt = if ($Asm) { "/Fc" } else { "/Fo" } + $out = & $fxcPath /nologo /T $Profile /E $Entry @defArgs /I $incDir $src $fmt $outFile 2>&1 + return @{ Code = $LASTEXITCODE; Out = $out } + } + + Write-Host "Shader : $relPath" + Write-Host "Base ref : $BaseRef (full include tree materialized)" + Write-Host "Profile : $Profile (entry $Entry)" + Write-Host "Include : $IncludeDir" + Write-Host ("-" * 60) + + $allIdentical = $true + $anyError = $false + + foreach ($perm in $Permutations) { + $tag = $perm + $baseCso = Join-Path $work "base.cso" + $workCso = Join-Path $work "work.cso" + $rb = Compile $baseFile $baseInclude $perm $baseCso + $rw = Compile $shaderFull $IncludeDir $perm $workCso + + if ($rb.Code -ne 0 -or $rw.Code -ne 0) { + $anyError = $true + $which = if ($rb.Code -ne 0) { "BASE" } else { "WORK" } + Write-Host "[$tag] COMPILE-ERROR ($which)" -ForegroundColor Red + ($(if ($rb.Code -ne 0) { $rb.Out } else { $rw.Out }) | Where-Object { $_ -match 'error|warning' } | Select-Object -First 6) | + ForEach-Object { Write-Host " $_" } + continue + } + + $hb = (Get-FileHash $baseCso -Algorithm SHA256).Hash + $hw = (Get-FileHash $workCso -Algorithm SHA256).Hash + if ($hb -eq $hw) { + Write-Host "[$tag] IDENTICAL" -ForegroundColor Green + } else { + $allIdentical = $false + Write-Host "[$tag] DIFFERS base=$($hb.Substring(0,12)) work=$($hw.Substring(0,12))" -ForegroundColor Yellow + # Tier 2: assembly diff for inspection (Compare-Object avoids git's CRLF/exit noise). + $baseAsm = Join-Path $work "base.asm"; $workAsm = Join-Path $work "work.asm" + Compile $baseFile $baseInclude $perm $baseAsm -Asm | Out-Null + Compile $shaderFull $IncludeDir $perm $workAsm -Asm | Out-Null + $d = Compare-Object (Get-Content $baseAsm) (Get-Content $workAsm) + if ($d) { + $d | Select-Object -First 40 | ForEach-Object { + $mark = if ($_.SideIndicator -eq '=>') { 'work' } else { 'base' } + Write-Host (" [{0}] {1}" -f $mark, $_.InputObject) + } + if (@($d).Count -gt 40) { Write-Host (" ... (+{0} more asm lines)" -f (@($d).Count - 40)) } + } + } + } + + Write-Host ("-" * 60) + if ($anyError) { Write-Host "RESULT: compile error" -ForegroundColor Red; $exit = 1 } + elseif ($allIdentical) { Write-Host "RESULT: behavior-preserving (all permutations identical)" -ForegroundColor Green; $exit = 0 } + else { Write-Host "RESULT: bytecode differs - inspect asm diff above" -ForegroundColor Yellow; $exit = 2 } + + exit $exit +} +finally { + # Runs on normal exit and on throw, so the temp dir never leaks. + if ($work -and (Test-Path $work)) { Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue } + Pop-Location +} diff --git a/tools/verify-shader-refactor.sh b/tools/verify-shader-refactor.sh new file mode 100644 index 0000000000..6f87588956 --- /dev/null +++ b/tools/verify-shader-refactor.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Thin wrapper around verify-shader-refactor.ps1 for bash/WSL/git-bash users. +# fxc.exe is Windows-only and MSYS mangles its /switches, so the real work lives +# in PowerShell. This just forwards arguments verbatim. +# +# Usage: tools/verify-shader-refactor.sh package/Shaders/Foo.hlsl [-BaseRef HEAD~1] ... +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ps1="$here/verify-shader-refactor.ps1" + +if command -v pwsh >/dev/null 2>&1; then + exec pwsh -NoProfile -ExecutionPolicy Bypass -File "$ps1" "$@" +elif command -v powershell.exe >/dev/null 2>&1; then + exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1" "$@" +else + echo "Need PowerShell (pwsh or powershell.exe) on PATH." >&2 + exit 1 +fi From 9ff72fafa15c567a358f7bab0738303003570620 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:17:05 +0200 Subject: [PATCH 22/55] fix(truepbr): honor DisableTerrainVertexColors on PBR landscape (#2463) --- package/Shaders/Lighting.hlsl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index fc3fbef07c..914aca708f 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2284,8 +2284,16 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) material.Metallic = saturate(rawRMAOS.y); material.AO = rawRMAOS.z; - // Apply vertex color to base color so PBR metals use it - float3 pbrVertexColor = Color::SrgbToLinear(input.Color.xyz); + // Apply vertex color to base color so PBR metals use it. On LANDSCAPE, + // honor DisableTerrainVertexColors (as the non-PBR path does) by + // neutralizing the source color so terrain vertex colors don't tint PBR; + // a white source yields VertexAO == 1, i.e. no AO darkening either. + float3 pbrVertexColorSrc = input.Color.xyz; +# if defined(LANDSCAPE) + if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) + pbrVertexColorSrc = 1; +# endif + float3 pbrVertexColor = Color::SrgbToLinear(pbrVertexColorSrc); float pbrVertexAO = max(max(pbrVertexColor.x, pbrVertexColor.y), pbrVertexColor.z); pbrVertexColor = pbrVertexAO == 0.0f ? 1.0f : pbrVertexColor * lerp(1 / max(pbrVertexAO, 0.001), 1, SharedData::truePBRSettings.VertexAOStrength); From 4d66c32e11e9391037419a0496bbf98cd6ec231e Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:22:15 +0200 Subject: [PATCH 23/55] fix(lighting): guard EMAT parallax shadow against undefined TBN (#2462) --- package/Shaders/Lighting.hlsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 914aca708f..75784bdd6b 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2913,7 +2913,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float parallaxShadow = 1; -# if defined(EMAT) +# if defined(EMAT) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) [branch] if ( SharedData::extendedMaterialSettings.EnableShadows && !(light.lightFlags & LightLimitFix::LightFlags::Simple) && @@ -2943,7 +2943,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) parallaxShadow = ExtendedMaterials::GetParallaxSoftShadowMultiplier(uv, mipLevel, lightDirectionTS, sh0, TexParallaxSampler, SampParallaxSampler, 0, parallaxShadowQuality, screenNoise, displacementParams); # endif } -# endif +# endif // defined(EMAT) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) DirectContext pointLightContext; DirectLightingOutput pointLightOutput; From 1561e4c783416a71188644d682df536c12a11a46 Mon Sep 17 00:00:00 2001 From: jiayev Date: Wed, 3 Jun 2026 21:08:20 +0800 Subject: [PATCH 24/55] fix(vol-fog): add missing include (#2466) --- package/Shaders/Sky.hlsl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 219962495b..4906306410 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -193,6 +193,10 @@ cbuffer AlphaTestRefCB : register(b11) # include "CloudShadows/CloudShadows.hlsli" # endif +# if defined(EXP_HEIGHT_FOG) +# include "ExponentialHeightFog/ExponentialHeightFog.hlsli" +# endif + # ifdef HDR_OUTPUT # include "HDRDisplay/HDRSun.hlsli" # include "Common/Random.hlsli" From 7cbe43f8d9f5cdc33db97ab66e9582fd8236ade7 Mon Sep 17 00:00:00 2001 From: jiayev Date: Wed, 3 Jun 2026 22:59:58 +0800 Subject: [PATCH 25/55] fix(i18n): add 'ready_for_review' type to pull request triggers (#2467) --- .github/workflows/pr-i18n.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-i18n.yaml b/.github/workflows/pr-i18n.yaml index 4b63de8252..3279da9005 100644 --- a/.github/workflows/pr-i18n.yaml +++ b/.github/workflows/pr-i18n.yaml @@ -2,7 +2,7 @@ name: "PR: i18n Check" on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review] paths: - "src/**" - "package/SKSE/Plugins/CommunityShaders/Translations/**" @@ -15,6 +15,7 @@ permissions: jobs: i18n-check: name: Verify en.json is in sync with source + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From c412e20abc4cde87903cc07ac4013ecce873a832 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Thu, 4 Jun 2026 03:03:16 +0200 Subject: [PATCH 26/55] fix(vol-fog): missing define (#2468) --- package/Shaders/Sky.hlsl | 1 + 1 file changed, 1 insertion(+) diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 4906306410..1d4be8643c 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -194,6 +194,7 @@ cbuffer AlphaTestRefCB : register(b11) # endif # if defined(EXP_HEIGHT_FOG) +# define SampColorSampler SampBaseSampler # include "ExponentialHeightFog/ExponentialHeightFog.hlsli" # endif From 38d3ce888c0df3aaa78d592cbd2aa04c9b983866 Mon Sep 17 00:00:00 2001 From: jiayev Date: Thu, 4 Jun 2026 16:22:13 +0800 Subject: [PATCH 27/55] fix(sss): clamp skin albedo to prevent explosion (#2469) --- package/Shaders/Common/Math.hlsli | 15 ++++++++------- package/Shaders/Lighting.hlsl | 12 ++++++++---- package/Shaders/Tests/TestMath.hlsl | 8 +++++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/package/Shaders/Common/Math.hlsli b/package/Shaders/Common/Math.hlsli index dc239bdafc..b9050e7bc0 100644 --- a/package/Shaders/Common/Math.hlsli +++ b/package/Shaders/Common/Math.hlsli @@ -1,13 +1,14 @@ #ifndef __MATH_DEPENDENCY_HLSL__ #define __MATH_DEPENDENCY_HLSL__ -#define EPSILON_SSS_ALBEDO 1e-3f // For albedo clamping in SSS calculations -#define EPSILON_DOT_CLAMP 1e-5f // For dot product clamping -#define EPSILON_DEPTH_SKY 1e-5f // Depth threshold for sky/unrendered pixel detection (raw reversed-Z near zero) -#define EPSILON_DIVISION 1e-6f // For division to avoid division by zero -#define EPSILON_GLINTS 1e-8f // For glints calculations -#define EPSILON_WEIGHT_SUM 1e-10f // For weight normalization -#define EPSILON_LENGTH_SQ 1e-20f // Minimum dot(v,v) before rsqrt to avoid inf on degenerate vectors +#define EPSILON_SSS_ALBEDO 1e-3f // For albedo clamping in SSS calculations +#define EPSILON_SKIN_ALBEDO 0.001f // Minimum per-channel skin base color to prevent SSS division explosion +#define EPSILON_DOT_CLAMP 1e-5f // For dot product clamping +#define EPSILON_DEPTH_SKY 1e-5f // Depth threshold for sky/unrendered pixel detection (raw reversed-Z near zero) +#define EPSILON_DIVISION 1e-6f // For division to avoid division by zero +#define EPSILON_GLINTS 1e-8f // For glints calculations +#define EPSILON_WEIGHT_SUM 1e-10f // For weight normalization +#define EPSILON_LENGTH_SQ 1e-20f // Minimum dot(v,v) before rsqrt to avoid inf on degenerate vectors #define DEPTH_SKY_SENTINEL 999999.0f // Linearized depth sentinel for sky/unmapped pixels (beyond any real geometry) diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 75784bdd6b..6af6545433 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -1124,12 +1124,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) complexMaterial = mipSample.w < (1.0 - kMaskEpsilon); const bool grayscaleMask = (abs(mipSample.x - mipSample.y) < kMaskEpsilon) && - (abs(mipSample.x - mipSample.z) < kMaskEpsilon) && - (abs(mipSample.y - mipSample.z) < kMaskEpsilon); + (abs(mipSample.x - mipSample.z) < kMaskEpsilon) && + (abs(mipSample.y - mipSample.z) < kMaskEpsilon); // Preserve height-only masks while rejecting grayscale environment masks const bool solidBlackHeightMask = all(mipSample.xyz < kMaskEpsilon) && - mipSample.w > kMaskEpsilon && - mipSample.w < (1.0 - kMaskEpsilon); + mipSample.w > kMaskEpsilon && + mipSample.w < (1.0 - kMaskEpsilon); if (grayscaleMask && !solidBlackHeightMask) complexMaterial = false; @@ -2429,6 +2429,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif // CS_SKIN +# if defined(SKIN) + material.BaseColor = max(material.BaseColor, EPSILON_SKIN_ALBEDO); +# endif + # if defined(CS_HAIR) && defined(HAIR) if (SharedData::hairSpecularSettings.Enabled) { material.Shininess = SharedData::hairSpecularSettings.HairGlossiness; diff --git a/package/Shaders/Tests/TestMath.hlsl b/package/Shaders/Tests/TestMath.hlsl index b2015618f2..7f8454678d 100644 --- a/package/Shaders/Tests/TestMath.hlsl +++ b/package/Shaders/Tests/TestMath.hlsl @@ -42,9 +42,15 @@ ASSERT(IsTrue, EPSILON_DIVISION < 0.00001f); ASSERT(AreEqual, EPSILON_DIVISION, 1e-6f); - // Verify ordering: DIVISION < DOT_CLAMP < SSS_ALBEDO + // EPSILON_SKIN_ALBEDO should be 0.001f + ASSERT(IsTrue, EPSILON_SKIN_ALBEDO > 0.0f); + ASSERT(IsTrue, EPSILON_SKIN_ALBEDO < 0.01f); + ASSERT(AreEqual, EPSILON_SKIN_ALBEDO, 0.001f); + + // Verify ordering: DIVISION < DOT_CLAMP < SSS_ALBEDO = SKIN_ALBEDO ASSERT(IsTrue, EPSILON_DIVISION < EPSILON_DOT_CLAMP); ASSERT(IsTrue, EPSILON_DOT_CLAMP < EPSILON_SSS_ALBEDO); + ASSERT(AreEqual, EPSILON_SSS_ALBEDO, EPSILON_SKIN_ALBEDO); } /// @tags math, matrix From e7194b8ca5d277d75f1299a2719fbd8b7f986607 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:40:02 +0200 Subject: [PATCH 28/55] chore(cs-editor): reduce logging (#2471) --- src/Features/CSEditor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index 98e2368e50..8fd88bccac 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -46,7 +46,10 @@ bool CSEditor::HasWidgetJsonFiles() std::error_code ec; const bool isDirectory = std::filesystem::is_directory(widgetSettingsPath, ec); if (ec) { - logger::warn("[CSEditor] Failed to inspect widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); + // A missing folder is the normal case (the user simply has no saved + // widgets for this category), so don't treat it as a warning. + if (ec != std::errc::no_such_file_or_directory) + logger::warn("[CSEditor] Failed to inspect widget settings path '{}': {}", widgetSettingsPath.string(), ec.message()); continue; } if (!isDirectory) @@ -60,6 +63,7 @@ bool CSEditor::HasWidgetJsonFiles() continue; } if (isRegularFile && _stricmp(it->path().extension().string().c_str(), kJsonExtension) == 0) { + logger::info("[CSEditor] Detected widget settings in '{}'", widgetSettingsPath.string()); s_hasWidgetJsonFiles = true; s_checkedWidgetJsonFiles = true; return true; From d1f406042a2531e086f2c233a2511fba182a0e19 Mon Sep 17 00:00:00 2001 From: Dlizzio <77717521+Dlizzio@users.noreply.github.com> Date: Fri, 5 Jun 2026 06:26:44 -0700 Subject: [PATCH 29/55] fix(UI): hide profiling when data not available (#2473) --- src/Menu/FeatureListRenderer.cpp | 2 +- src/Menu/ProfilingRenderer.cpp | 24 ++++++++++++++++++++++-- src/Menu/ProfilingRenderer.h | 4 ++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index c77adda583..c0723b8358 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -812,7 +812,7 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImVec2 cursorPosBefore = ImGui::GetCursorPos(); feat->DrawSettings(); - if (feat != &globals::features::csEditor) { + if (feat != &globals::features::csEditor && ProfilingRenderer::HasFeatureTimers(feat->GetShortName())) { ImGui::SeparatorText(T("menu.features.profiling", "Profiling")); ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); } diff --git a/src/Menu/ProfilingRenderer.cpp b/src/Menu/ProfilingRenderer.cpp index 67912426de..60ede4e6a5 100644 --- a/src/Menu/ProfilingRenderer.cpp +++ b/src/Menu/ProfilingRenderer.cpp @@ -370,9 +370,9 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) float maxP95 = 0.0f; float maxP99 = 0.0f; - std::string prefix = featurePrefix + "::"; + const auto prefix = GetFeatureTimerPrefix(featurePrefix); for (const auto& r : results) { - if (!r.valid || !r.name.starts_with(prefix)) + if (!IsFeatureTimerResult(r, prefix)) continue; std::string label = r.name.substr(prefix.size()); float timeMs = cpuMode ? r.cpuTimeMs : r.gpuTimeMs; @@ -451,3 +451,23 @@ void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) ImGui::EndTable(); } } + +bool ProfilingRenderer::HasFeatureTimers(const std::string& featurePrefix) +{ + const auto prefix = GetFeatureTimerPrefix(featurePrefix); + const auto& results = globals::profiler->GetResults(); + + return std::ranges::any_of(results, [&prefix](const auto& result) { + return IsFeatureTimerResult(result, prefix); + }); +} + +std::string ProfilingRenderer::GetFeatureTimerPrefix(const std::string& featurePrefix) +{ + return featurePrefix + "::"; +} + +bool ProfilingRenderer::IsFeatureTimerResult(const Profiler::TimerResult& result, std::string_view prefix) +{ + return result.valid && result.name.starts_with(prefix); +} diff --git a/src/Menu/ProfilingRenderer.h b/src/Menu/ProfilingRenderer.h index 9b8be57c97..c247e58e25 100644 --- a/src/Menu/ProfilingRenderer.h +++ b/src/Menu/ProfilingRenderer.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -20,6 +21,7 @@ class ProfilingRenderer static void RenderStatistics(bool showTable = true, bool showModeToggle = true); static void RenderFeatureTimers(const std::string& featurePrefix); + static bool HasFeatureTimers(const std::string& featurePrefix); private: static inline TimingMode timingMode = TimingMode::GPU; @@ -67,4 +69,6 @@ class ProfilingRenderer static void RenderTimingModeToggle(); static void SetupTimingTableColumns(bool includePercentColumn); static void RenderGraph(); + static std::string GetFeatureTimerPrefix(const std::string& featurePrefix); + static bool IsFeatureTimerResult(const Profiler::TimerResult& result, std::string_view prefix); }; From eda4e97dab3baa26cf11198c4a6e13d7f087da21 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sat, 6 Jun 2026 09:35:25 +0100 Subject: [PATCH 30/55] fix(unified water): add IsDisabledByDefault (#2474) --- src/Feature.h | 7 +++++++ src/Features/UnifiedWater.h | 1 + src/State.cpp | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/Feature.h b/src/Feature.h index adc3ba32fb..a82064acaf 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -73,6 +73,13 @@ struct Feature */ virtual std::string_view GetCategory() const { return FeatureCategories::kOther; } + /** + * Whether the feature is disabled at boot by default (before any user override). + * Features that override this to return true will start disabled on first install; + * users can still enable them via the "Disable at Boot" menu. + */ + virtual bool IsDisabledByDefault() const { return false; } + /** * Whether the feature will show up in the GUI menu */ diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 4a1e616c33..0de2594480 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -105,6 +105,7 @@ struct UnifiedWater : OverlayFeature virtual void RestoreDefaultSettings() override; virtual bool IsCore() const override { return true; } + virtual bool IsDisabledByDefault() const override { return true; } virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/State.cpp b/src/State.cpp index bf50000151..6ddb97af5f 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -366,6 +366,10 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) for (auto* feature : Feature::GetFeatureList()) { try { const std::string featureName = feature->GetShortName(); + if (!disabledFeatures.contains(featureName) && feature->IsDisabledByDefault()) { + disabledFeatures[featureName] = true; + logger::info("Feature '{}' is disabled by default", featureName); + } bool isDisabled = disabledFeatures.contains(featureName) && disabledFeatures[featureName]; if (!isDisabled) { logger::info("Loading Feature: '{}'", featureName); From e368f30d387a8ff20fa8e691984b94702d586f44 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:12:27 +0200 Subject: [PATCH 31/55] ci: use prebuilt CommonLib (#2476) Co-authored-by: Alan Tse Co-authored-by: Claude Opus 4.8 --- CMakePresets.json | 16 +++++++++++++--- extern/CommonLibSSE-NG | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 790a0b13ad..70e9da7bb7 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -57,7 +57,8 @@ "ENABLE_SKYRIM_AE": "ON", "ENABLE_SKYRIM_SE": "ON", "ENABLE_SKYRIM_VR": "ON", - "AUTO_PLUGIN_DEPLOYMENT": "OFF" + "AUTO_PLUGIN_DEPLOYMENT": "OFF", + "COMMONLIB_PREBUILT_MULTICONFIG": "ON" }, "inherits": "skyrim" }, @@ -68,9 +69,18 @@ "ENABLE_SKYRIM_AE": "ON", "ENABLE_SKYRIM_SE": "ON", "ENABLE_SKYRIM_VR": "ON", - "AUTO_PLUGIN_DEPLOYMENT": "OFF" + "AUTO_PLUGIN_DEPLOYMENT": "OFF", + "COMMONLIB_PREBUILT_MULTICONFIG": "ON" }, "inherits": "skyrim" + }, + { + "name": "ALL-DEBUG", + "description": "Like ALL but builds CommonLib from source. The prebuilt package is Release-only (Debug maps to no imported location), so a Debug build off ALL fails to link — this preset keeps Debug-from-source working in its own build/ALL-DEBUG dir.", + "inherits": "ALL", + "cacheVariables": { + "COMMONLIB_PREBUILT_MULTICONFIG": "OFF" + } } ], "buildPresets": [ @@ -112,7 +122,7 @@ { "name": "Debug", "description": "Debug build for CS SKSE plugin, generate an AIO folder thats ready to copy", - "configurePreset": "ALL", + "configurePreset": "ALL-DEBUG", "configuration": "Debug", "targets": ["CommunityShaders", "AIO"] } diff --git a/extern/CommonLibSSE-NG b/extern/CommonLibSSE-NG index 8c4025b01f..8f4205da56 160000 --- a/extern/CommonLibSSE-NG +++ b/extern/CommonLibSSE-NG @@ -1 +1 @@ -Subproject commit 8c4025b01fac2bea1bbe73a3a9da7b4fde338343 +Subproject commit 8f4205da56f01cbe98557422c008a5719c180fb9 From 4d42fbed5d039a80fa976339fcce35279b71253a Mon Sep 17 00:00:00 2001 From: davo0411 Date: Sat, 6 Jun 2026 19:59:43 +1000 Subject: [PATCH 32/55] fix: dynamic cubemaps X4000 warning (#2478) --- .../DynamicCubemaps/DynamicCubemaps.hlsli | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli index 515c440586..e28e7d0e30 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli @@ -41,13 +41,12 @@ namespace DynamicCubemaps # if defined(IBL) && defined(LIGHTING) const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection); - if (SharedData::iblSettings.EnableIBL && SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection) { - float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; - finalIrradiance = specularIrradiance; - return finalIrradiance; - } + const bool useStaticIBL = SharedData::iblSettings.EnableIBL && SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection; +# else + const bool useStaticIBL = false; # endif + if (!useStaticIBL) { # if defined(SKYLIGHTING) float skylightingSpecular = 0.0; if (!SharedData::InInterior) { @@ -59,7 +58,8 @@ namespace DynamicCubemaps if (SharedData::iblSettings.EnableIBL) { float3 envSample = EnvTexture.SampleLevel(SampColorSampler, R, level); float3 fullSample = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); - float3 envSpecular, skySpecular; + float3 envSpecular = 0.0; + float3 skySpecular = 0.0; if (SharedData::iblSettings.DALCMode == 2) { // Mode 2: DALC-normalized env scaled by DALCAmount + sky overlay @@ -118,6 +118,12 @@ namespace DynamicCubemaps finalIrradiance = Color::IrradianceToLinear(specularIrradiance); # endif } + } else { +# if defined(IBL) && defined(LIGHTING) + float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; + finalIrradiance = specularIrradiance; +# endif + } return finalIrradiance; # endif @@ -147,8 +153,7 @@ namespace DynamicCubemaps const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection); if (SharedData::iblSettings.EnableIBL && SharedData::iblSettings.UseStaticIBL && !inWorld && !inReflection) { float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; - finalIrradiance += specularIrradiance; - return (F0 * specularBRDF.x + specularBRDF.y) * finalIrradiance; + return (F0 * specularBRDF.x + specularBRDF.y) * specularIrradiance; } # endif @@ -163,7 +168,8 @@ namespace DynamicCubemaps if (SharedData::iblSettings.EnableIBL) { float3 envSample = EnvTexture.SampleLevel(SampColorSampler, R, level); float3 fullSample = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); - float3 envSpecular, skySpecular; + float3 envSpecular = 0.0; + float3 skySpecular = 0.0; if (SharedData::iblSettings.DALCMode == 2) { // Mode 2: DALC-normalized env scaled by DALCAmount + sky overlay From 16f6ede3beca62468eded49319e3c193be577f25 Mon Sep 17 00:00:00 2001 From: davo0411 Date: Sat, 6 Jun 2026 20:14:16 +1000 Subject: [PATCH 33/55] fix: triplanar.hlsli x4000 warning (#2479) --- package/Shaders/Common/Triplanar.hlsli | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/package/Shaders/Common/Triplanar.hlsli b/package/Shaders/Common/Triplanar.hlsli index caffec5a98..cf4d822ea2 100644 --- a/package/Shaders/Common/Triplanar.hlsli +++ b/package/Shaders/Common/Triplanar.hlsli @@ -36,11 +36,14 @@ namespace Triplanar float3 dPdy = 0.0; ComputeGradients(worldPos, scale, dPdx, dPdy); + float4 result = 0; if (noise < weights.x) - return tex.SampleGrad(samp, worldPos.yz * scale, dPdx.yz, dPdy.yz); - if (noise < weights.x + weights.y) - return tex.SampleGrad(samp, worldPos.xz * scale, dPdx.xz, dPdy.xz); - return tex.SampleGrad(samp, worldPos.xy * scale, dPdx.xy, dPdy.xy); + result = tex.SampleGrad(samp, worldPos.yz * scale, dPdx.yz, dPdy.yz); + else if (noise < weights.x + weights.y) + result = tex.SampleGrad(samp, worldPos.xz * scale, dPdx.xz, dPdy.xz); + else + result = tex.SampleGrad(samp, worldPos.xy * scale, dPdx.xy, dPdy.xy); + return result; } /// Stochastic triplanar with mip bias via gradient scaling. @@ -53,11 +56,14 @@ namespace Triplanar dPdx *= biasScale; dPdy *= biasScale; + float4 result = 0; if (noise < weights.x) - return tex.SampleGrad(samp, worldPos.yz * scale, dPdx.yz, dPdy.yz); - if (noise < weights.x + weights.y) - return tex.SampleGrad(samp, worldPos.xz * scale, dPdx.xz, dPdy.xz); - return tex.SampleGrad(samp, worldPos.xy * scale, dPdx.xy, dPdy.xy); + result = tex.SampleGrad(samp, worldPos.yz * scale, dPdx.yz, dPdy.yz); + else if (noise < weights.x + weights.y) + result = tex.SampleGrad(samp, worldPos.xz * scale, dPdx.xz, dPdy.xz); + else + result = tex.SampleGrad(samp, worldPos.xy * scale, dPdx.xy, dPdy.xy); + return result; } } From 4dff23fd977330c96ea1eafee3962cd4ff2c669d Mon Sep 17 00:00:00 2001 From: davo0411 Date: Sun, 7 Jun 2026 09:19:30 +1000 Subject: [PATCH 34/55] feat(UI): custom cursor support (#2480) --- .../CommunityShaders/Translations/en.json | 3 + src/Menu.cpp | 107 ++++++++++ src/Menu.h | 19 ++ src/Menu/CursorLoader.cpp | 202 ++++++++++++++++++ src/Menu/CursorLoader.h | 12 ++ src/Menu/IconLoader.cpp | 34 ++- src/Menu/OverlayRenderer.cpp | 5 + src/Menu/SettingsTabRenderer.cpp | 20 ++ src/Menu/ThemeManager.cpp | 1 - src/Menu/ThemeManager.h | 8 + src/Utils/FileSystem.cpp | 5 + src/Utils/FileSystem.h | 6 + 12 files changed, 403 insertions(+), 19 deletions(-) create mode 100644 src/Menu/CursorLoader.cpp create mode 100644 src/Menu/CursorLoader.h diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 4e6dc83057..e4881b7930 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1962,6 +1962,7 @@ "menu.settings.create_new_theme_hint": "Create a new theme with your current settings:", "menu.settings.create_theme": "Create Theme", "menu.settings.cs_editor_toggle_key": "CS Editor Toggle Key:", + "menu.settings.custom_cursor_status": "Custom cursor images loaded", "menu.settings.delete_button": "Delete", "menu.settings.delete_theme": "Delete", "menu.settings.delete_theme_confirm_part1": "Are you sure you want to delete the theme '", @@ -2089,6 +2090,8 @@ "menu.settings.track_opacity": "Track Opacity", "menu.settings.track_opacity_tooltip": "Controls the opacity of the scrollbar track/channel (the background area behind the scrollbar).", "menu.settings.ui_behavior": "UI Behavior", + "menu.settings.use_custom_cursor": "Use Custom Theme Cursor", + "menu.settings.use_custom_cursor_tooltip": "Loads cursor PNGs from the active theme folder (Themes//).\nSupported files include cursor.png (arrow), cursor_text.png (typing), cursor_resize_ew.png, cursor_resize_ns.png, and more.\nMissing types fall back to the default ImGui cursor. Configure per-type hotspots in theme JSON.", "menu.settings.use_custom_shaders": "Use Custom Shaders", "menu.settings.use_custom_shaders_tooltip": "Disabling this effectively disables all features.", "menu.settings.use_monochrome_cs_logo": "Use Monochrome CS Logo", diff --git a/src/Menu.cpp b/src/Menu.cpp index f9b3f44b03..719c90f0a3 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -30,6 +30,7 @@ #include "Menu/FeatureListRenderer.h" #include "Menu/Fonts.h" #include "Menu/HomePageRenderer.h" +#include "Menu/CursorLoader.h" #include "Menu/IconLoader.h" #include "Menu/MenuHeaderRenderer.h" #include "Menu/OverlayRenderer.h" @@ -136,6 +137,19 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( DockingSeparatorSize, MouseCursorScale) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::ThemeSettings::CursorImageSettings, + File, + HotspotX, + HotspotY) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Menu::ThemeSettings::CursorSettings, + Scale, + File, + HotspotX, + HotspotY) + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings, FontSize, @@ -150,6 +164,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( CenterHeader, TooltipHoverDelay, BackgroundBlurEnabled, + UseCustomCursor, + Cursor, ScrollbarOpacity, Palette, StatusPalette, @@ -178,6 +194,69 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( bool IsEnabled = false; std::unordered_map Menu::categoryCounts; +namespace +{ + struct CursorTypeKey + { + const char* key; + ImGuiMouseCursor type; + }; + + constexpr CursorTypeKey kCursorTypeKeys[] = { + { "Arrow", ImGuiMouseCursor_Arrow }, + { "TextInput", ImGuiMouseCursor_TextInput }, + { "ResizeAll", ImGuiMouseCursor_ResizeAll }, + { "ResizeNS", ImGuiMouseCursor_ResizeNS }, + { "ResizeEW", ImGuiMouseCursor_ResizeEW }, + { "ResizeNESW", ImGuiMouseCursor_ResizeNESW }, + { "ResizeNWSE", ImGuiMouseCursor_ResizeNWSE }, + { "Hand", ImGuiMouseCursor_Hand }, + { "NotAllowed", ImGuiMouseCursor_NotAllowed }, + }; +} + +void Menu::CursorFromJson(const json& cursorJson, ThemeSettings::CursorSettings& cursor) +{ + cursor.Types = {}; + + if (!cursorJson.contains("Types")) { + return; + } + + const auto& types = cursorJson["Types"]; + if (types.is_object()) { + for (const auto& [key, type] : kCursorTypeKeys) { + if (types.contains(key) && types[key].is_object()) { + types[key].get_to(cursor.Types[static_cast(type)]); + } + } + return; + } + + // Legacy: sparse array indexed by ImGuiMouseCursor_* + if (types.is_array()) { + for (size_t i = 0; i < ImGuiMouseCursor_COUNT && i < types.size(); ++i) { + if (types[i].is_object()) { + types[i].get_to(cursor.Types[i]); + } + } + } +} + +void Menu::CursorToJson(json& cursorJson, const ThemeSettings::CursorSettings& cursor) +{ + json types = json::object(); + for (const auto& [key, type] : kCursorTypeKeys) { + const auto& settings = cursor.Types[static_cast(type)]; + if (!settings.File.empty() || settings.HotspotX != 0.0f || settings.HotspotY != 0.0f) { + types[key] = settings; + } + } + if (!types.empty()) { + cursorJson["Types"] = types; + } +} + // Pad FontRoles JSON array with defaults if shorter than FontRole::Count. // Prevents deserialization failure when loading old settings with fewer font roles. static void SanitizeFontRolesJson(json& themeJson) @@ -307,6 +386,8 @@ Menu::~Menu() uiIcons.playMode.Release(); uiIcons.search.Release(); + Util::CursorLoader::Shutdown(); + // Clean up blur resources BackgroundBlur::Cleanup(); @@ -373,6 +454,9 @@ void Menu::Load(json& o_json) SanitizeFontRolesJson(o_json["Theme"]); settings.Theme = o_json["Theme"]; PaletteFromJson(o_json["Theme"], settings.Theme.FullPalette); + if (o_json["Theme"].contains("Cursor") && o_json["Theme"]["Cursor"].is_object()) { + CursorFromJson(o_json["Theme"]["Cursor"], settings.Theme.Cursor); + } MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; @@ -440,7 +524,11 @@ void Menu::LoadTheme(json& o_json) SanitizeFontRolesJson(o_json["Theme"]); settings.Theme = o_json["Theme"]; PaletteFromJson(o_json["Theme"], settings.Theme.FullPalette); + if (o_json["Theme"].contains("Cursor") && o_json["Theme"]["Cursor"].is_object()) { + CursorFromJson(o_json["Theme"]["Cursor"], settings.Theme.Cursor); + } MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); + Util::CursorLoader::MigrateLegacyCursorSettings(settings.Theme); auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; if (!Util::ValidateFont(bodyRole.File)) { @@ -469,6 +557,7 @@ void Menu::SaveTheme(json& o_json) o_json["Theme"] = settings.Theme; PaletteToJson(o_json["Theme"], settings.Theme.FullPalette); + CursorToJson(o_json["Theme"]["Cursor"], settings.Theme.Cursor); } std::vector Menu::DiscoverThemes() @@ -502,8 +591,12 @@ bool Menu::LoadThemePreset(const std::string& themeName) try { settings.Theme = themeSettings; PaletteFromJson(themeSettings, settings.Theme.FullPalette); + if (themeSettings.contains("Cursor") && themeSettings["Cursor"].is_object()) { + CursorFromJson(themeSettings["Cursor"], settings.Theme.Cursor); + } MenuFonts::NormalizeFontRoles(settings.Theme, hasFontRoles); + Util::CursorLoader::MigrateLegacyCursorSettings(settings.Theme); auto& bodyRole = settings.Theme.FontRoles[static_cast(FontRole::Body)]; if (!Util::ValidateFont(bodyRole.File)) { const auto& defaults = Menu::GetDefaultFontRole(FontRole::Body); @@ -522,6 +615,7 @@ bool Menu::LoadThemePreset(const std::string& themeName) // Schedule deferred icon reload to apply theme-specific icon overrides pendingIconReload = true; + pendingCursorReload = true; // Apply background blur enabled state from theme BackgroundBlur::SetEnabled(settings.Theme.BackgroundBlurEnabled); @@ -629,6 +723,8 @@ void Menu::Init() logger::warn("Menu::Init() - Failed to load UI icons. Will fallback to text buttons"); } + Util::CursorLoader::Reload(this); + // Initialize background blur system if (!BackgroundBlur::Initialize()) { logger::warn("Menu::Init() - Failed to initialize background blur system"); @@ -871,6 +967,17 @@ void Menu::DrawOverlay() } } + if (pendingCursorReload && canReload) { + static bool loggedCursorReloadRetry = false; + if (Util::CursorLoader::Reload(this)) { + pendingCursorReload = false; + loggedCursorReloadRetry = false; + } else if (!loggedCursorReloadRetry) { + logger::warn("Menu::DrawOverlay() - Cursor reload deferred (will retry when ready)"); + loggedCursorReloadRetry = true; + } + } + OverlayRenderer::RenderOverlay( *this, [this]() { ProcessInputEventQueue(); }, diff --git a/src/Menu.h b/src/Menu.h index 2add98b636..61079df656 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -166,6 +166,7 @@ class Menu // Deferred reload systems (public for SettingsTabRenderer access) bool pendingFontReload = false; bool pendingIconReload = false; + bool pendingCursorReload = false; // Display size tracking for cross-session resolution change detection float2 lastDisplaySize{}; @@ -265,6 +266,21 @@ class Menu bool CenterHeader = false; // whether to center the header title and logo float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds bool BackgroundBlurEnabled = true; // enable background blur effect + bool UseCustomCursor = false; // use theme cursor images instead of default ImGui cursors + struct CursorImageSettings + { + std::string File; + float HotspotX = 0.0f; + float HotspotY = 0.0f; + }; + struct CursorSettings + { + float Scale = 1.0f; + std::string File; // legacy arrow file (migrated into Types[Arrow] on load) + float HotspotX = 0.0f; + float HotspotY = 0.0f; + std::array Types = {}; + } Cursor; // Scrollbar opacity settings struct ScrollbarOpacitySettings { @@ -397,6 +413,9 @@ class Menu static void PaletteToJson(json& themeJson, const std::array& palette); static void PaletteFromJson(const json& themeJson, std::array& palette); + static void CursorToJson(json& cursorJson, const ThemeSettings::CursorSettings& cursorSettings); + static void CursorFromJson(const json& cursorJson, ThemeSettings::CursorSettings& cursor); + struct Settings { std::vector ToggleKey = { InputCombo::Keyboard(VK_END) }; diff --git a/src/Menu/CursorLoader.cpp b/src/Menu/CursorLoader.cpp new file mode 100644 index 0000000000..2ae20449a5 --- /dev/null +++ b/src/Menu/CursorLoader.cpp @@ -0,0 +1,202 @@ +#include "PCH.h" + +#include "CursorLoader.h" +#include "Menu.h" + +namespace Util::CursorLoader +{ + namespace + { + struct LoadedCursor + { + ID3D11ShaderResourceView* texture = nullptr; + ImVec2 size{}; + ImVec2 hotspot{}; + + void Release() + { + if (texture) { + texture->Release(); + texture = nullptr; + } + size = {}; + hotspot = {}; + } + }; + + eastl::array g_cursors = {}; + + void ForEachSlot(const Menu::ThemeSettings& theme, auto&& fn) + { + static constexpr struct { ImGuiMouseCursor cursor; const char* defaultFile; } kSlots[] = { + { ImGuiMouseCursor_Arrow, "cursor.png" }, + { ImGuiMouseCursor_TextInput, "cursor_text.png" }, + { ImGuiMouseCursor_ResizeAll, "cursor_resize_all.png" }, + { ImGuiMouseCursor_ResizeNS, "cursor_resize_ns.png" }, + { ImGuiMouseCursor_ResizeEW, "cursor_resize_ew.png" }, + { ImGuiMouseCursor_ResizeNESW, "cursor_resize_nesw.png" }, + { ImGuiMouseCursor_ResizeNWSE, "cursor_resize_nwse.png" }, + { ImGuiMouseCursor_Hand, "cursor_hand.png" }, + { ImGuiMouseCursor_NotAllowed, "cursor_not_allowed.png" }, + }; + for (const auto& slot : kSlots) { + fn(slot.cursor, slot.defaultFile, theme.Cursor.Types[static_cast(slot.cursor)]); + } + } + + std::string EffectiveFile(const Menu::ThemeSettings::CursorImageSettings& settings, const char* defaultFile) + { + return !settings.File.empty() ? settings.File : defaultFile; + } + + std::filesystem::path ResolvePath(const Menu& menu, const std::string& fileName) + { + if (fileName.empty()) { + return {}; + } + const auto& preset = menu.GetSettings().SelectedThemePreset; + if (!preset.empty()) { + auto themePath = Util::PathHelpers::GetThemesPath() / preset / fileName; + if (std::filesystem::exists(themePath)) { + return themePath; + } + } + auto sharedPath = Util::PathHelpers::GetCursorsPath() / fileName; + return std::filesystem::exists(sharedPath) ? sharedPath : std::filesystem::path{}; + } + + bool IsPathAllowed(const std::filesystem::path& path) + { + return Util::IsPathWithinDirectory(Util::PathHelpers::GetThemesPath(), path) || + Util::IsPathWithinDirectory(Util::PathHelpers::GetCursorsPath(), path); + } + } + + void MigrateLegacyCursorSettings(Menu::ThemeSettings& theme) + { + auto& types = theme.Cursor.Types; + auto& arrow = types[ImGuiMouseCursor_Arrow]; + if (arrow.File.empty() && !theme.Cursor.File.empty()) { + arrow.File = theme.Cursor.File; + arrow.HotspotX = theme.Cursor.HotspotX; + arrow.HotspotY = theme.Cursor.HotspotY; + } + } + + int GetLoadedCount() + { + int count = 0; + for (const auto& cursor : g_cursors) { + if (cursor.texture) { + ++count; + } + } + return count; + } + + void Shutdown() + { + for (auto& cursor : g_cursors) { + cursor.Release(); + } + } + + bool Reload(Menu* menu) + { + if (!menu) { + return false; + } + + Shutdown(); + + if (!menu->GetSettings().Theme.UseCustomCursor) { + return true; + } + + auto* device = globals::d3d::device; + static bool loggedMissingDevice = false; + if (!device) { + if (!loggedMissingDevice) { + logger::warn("CursorLoader::Reload: D3D device is null; will retry when available"); + loggedMissingDevice = true; + } + return false; + } + loggedMissingDevice = false; + + MigrateLegacyCursorSettings(menu->GetSettings().Theme); + const auto& theme = menu->GetSettings().Theme; + + int loadedCount = 0; + int failedCount = 0; + ForEachSlot(theme, [&](ImGuiMouseCursor cursor, const char* defaultFile, const Menu::ThemeSettings::CursorImageSettings& settings) { + const auto fileName = EffectiveFile(settings, defaultFile); + const auto path = ResolvePath(*menu, fileName); + if (path.empty() || !IsPathAllowed(path)) { + return; + } + + ID3D11ShaderResourceView* srv = nullptr; + ImVec2 size{}; + if (!Util::LoadTextureFromFile(device, path.string().c_str(), &srv, size)) { + ++failedCount; + return; + } + + auto& loaded = g_cursors[static_cast(cursor)]; + loaded.texture = srv; + loaded.size = size; + loaded.hotspot = ImVec2(settings.HotspotX, settings.HotspotY); + ++loadedCount; + }); + + if (loadedCount == 0) { + logger::warn("CursorLoader::Reload: No cursor images found under Themes// or Interface/CommunityShaders/Cursors/"); + } else { + if (failedCount > 0) { + logger::warn("CursorLoader::Reload: Loaded {} custom cursor image(s); {} file(s) failed to decode", loadedCount, failedCount); + } else { + logger::info("CursorLoader::Reload: Loaded {} custom cursor image(s)", loadedCount); + } + } + return true; + } + + void DrawCustomCursor(const Menu& menu) + { + const auto& theme = menu.GetSettings().Theme; + if (!theme.UseCustomCursor) { + return; + } + + auto& io = ImGui::GetIO(); + if (!io.MouseDrawCursor) { + return; + } + + const auto active = ImGui::GetMouseCursor(); + if (active <= ImGuiMouseCursor_None || active >= ImGuiMouseCursor_COUNT) { + return; + } + + const auto& loaded = g_cursors[static_cast(active)]; + if (!loaded.texture) { + return; + } + + ImGui::SetMouseCursor(ImGuiMouseCursor_None); + + const float scale = (theme.Cursor.Scale > 0.0f ? theme.Cursor.Scale : 1.0f) * ImGui::GetStyle().MouseCursorScale; + const ImVec2 drawSize{ loaded.size.x * scale, loaded.size.y * scale }; + const ImVec2 hotspot{ loaded.hotspot.x * scale, loaded.hotspot.y * scale }; + const ImVec2 pos{ io.MousePos.x - hotspot.x, io.MousePos.y - hotspot.y }; + + ImGui::GetForegroundDrawList()->AddImage( + reinterpret_cast(loaded.texture), + pos, + { pos.x + drawSize.x, pos.y + drawSize.y }, + {}, + { 1.0f, 1.0f }, + IM_COL32_WHITE); + } +} diff --git a/src/Menu/CursorLoader.h b/src/Menu/CursorLoader.h new file mode 100644 index 0000000000..020c819b64 --- /dev/null +++ b/src/Menu/CursorLoader.h @@ -0,0 +1,12 @@ +#pragma once + +class Menu; + +namespace Util::CursorLoader +{ + void MigrateLegacyCursorSettings(Menu::ThemeSettings& theme); + bool Reload(Menu* menu); + int GetLoadedCount(); + void Shutdown(); + void DrawCustomCursor(const Menu& menu); +} diff --git a/src/Menu/IconLoader.cpp b/src/Menu/IconLoader.cpp index d12f3ce69a..7d2dba0872 100644 --- a/src/Menu/IconLoader.cpp +++ b/src/Menu/IconLoader.cpp @@ -7,25 +7,13 @@ #include "Utils/D3D.h" #include "Utils/FileSystem.h" -#include -#include -#include - -#include #include #include -#include +#include #include -namespace Util::IconLoader +namespace Util { - struct IconDefinition - { - std::string filename; - ID3D11ShaderResourceView** texture; - ImVec2* size; - }; - bool LoadTextureFromFile(ID3D11Device* device, const char* filename, ID3D11ShaderResourceView** out_srv, ImVec2& out_size) { int image_width = 0; @@ -86,9 +74,19 @@ namespace Util::IconLoader pTexture->Release(); stbi_image_free(image_data); - out_size = ImVec2((float)image_width, (float)image_height); + out_size = ImVec2(static_cast(image_width), static_cast(image_height)); return true; } +} + +namespace Util::IconLoader +{ + struct IconDefinition + { + std::string filename; + ID3D11ShaderResourceView** texture; + ImVec2* size; + }; std::vector GetIconDefinitions(Menu* menu) { @@ -156,7 +154,7 @@ namespace Util::IconLoader *iconDef.texture = nullptr; } - if (LoadTextureFromFile(device, iconPath.string().c_str(), iconDef.texture, *iconDef.size)) { + if (Util::LoadTextureFromFile(device, iconPath.string().c_str(), iconDef.texture, *iconDef.size)) { logger::debug("LoadThemeSpecificIcons: Loaded custom icon: {}", iconPath.filename().string()); iconsOverridden++; } @@ -217,7 +215,7 @@ namespace Util::IconLoader for (const auto& iconDef : iconDefs) { std::string fullPath = basePath + iconDef.filename; - if (LoadTextureFromFile(device, fullPath.c_str(), iconDef.texture, *iconDef.size)) { + if (Util::LoadTextureFromFile(device, fullPath.c_str(), iconDef.texture, *iconDef.size)) { iconsLoaded++; anyIconLoaded = true; } else { @@ -228,7 +226,7 @@ namespace Util::IconLoader if (pos != std::string::npos) { fallbackPath.erase(pos, 11); // Remove "\Monochrome" } - if (LoadTextureFromFile(device, fallbackPath.c_str(), iconDef.texture, *iconDef.size)) { + if (Util::LoadTextureFromFile(device, fallbackPath.c_str(), iconDef.texture, *iconDef.size)) { iconsLoaded++; anyIconLoaded = true; } else { diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 3f31d5ca8d..58471ac9ef 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -19,6 +19,7 @@ #include "Menu.h" #include "ShaderCache.h" #include "State.h" +#include "Menu/CursorLoader.h" #include "Util.h" #include "Features/PerformanceOverlay.h" @@ -379,6 +380,10 @@ void OverlayRenderer::HandleABTesting() void OverlayRenderer::FinalizeImGuiFrame() { + if (auto* menu = Menu::GetSingleton()) { + Util::CursorLoader::DrawCustomCursor(*menu); + } + ImGui::Render(); // Apply background blur behind ImGui windows before rendering them diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 79b2ae774a..804c7c6ea8 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -11,6 +11,7 @@ #include "Fonts.h" #include "Globals.h" #include "I18n/I18n.h" +#include "CursorLoader.h" #include "IconLoader.h" #include "Menu.h" #include "ShaderCache.h" @@ -516,6 +517,25 @@ void SettingsTabRenderer::RenderBehaviorTab() ImGui::TextUnformatted(T("menu.settings.tooltip_hover_delay_tooltip", "Time in seconds to wait before a tooltip appears when hovering over an item.")); } + if (ImGui::Checkbox(T("menu.settings.use_custom_cursor", "Use Custom Theme Cursor"), &themeSettings.UseCustomCursor)) { + globals::menu->pendingCursorReload = true; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("menu.settings.use_custom_cursor_tooltip", + "Loads cursor PNGs from the active theme folder (Themes//).\n" + "Supported files include cursor.png (arrow), cursor_text.png (typing), cursor_resize_ew.png, cursor_resize_ns.png, and more.\n" + "Missing types fall back to the default ImGui cursor. Configure per-type hotspots in theme JSON.")); + } + + if (themeSettings.UseCustomCursor) { + ImGui::Indent(); + const int loadedCount = Util::CursorLoader::GetLoadedCount(); + ImGui::TextDisabled("%s: %d", + T("menu.settings.custom_cursor_status", "Custom cursor images loaded"), + loadedCount); + ImGui::Unindent(); + } + SeparatorTextWithFont(T("menu.settings.visual_effects", "Visual Effects"), Menu::FontRole::Subheading); if (ImGui::Checkbox(T("menu.settings.background_blur", "Background Blur"), &themeSettings.BackgroundBlurEnabled)) { diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 94b8533c1f..b43784f8c6 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -210,7 +210,6 @@ void ThemeManager::SetupImGuiStyle(const Menu& menu) styleCopy.SeparatorTextBorderSize = scaleSize(themeSettings.Style.SeparatorTextBorderSize); styleCopy.DockingSeparatorSize = scaleSize(themeSettings.Style.DockingSeparatorSize); - styleCopy.MouseCursorScale = 1.f; style = styleCopy; style.HoverDelayNormal = themeSettings.TooltipHoverDelay; style.FontScaleMain = exp2(globalScale); diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 856e9d51c8..707b38af33 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -38,6 +38,14 @@ using json = nlohmann::json; * * "TooltipHoverDelay": 0.5, // Seconds before tooltip appears * "ShowActionIcons": true, // Show icons on action buttons + * "UseCustomCursor": false, + * "Cursor": { + * "Scale": 1.0, + * "Types": { + * "Arrow": { "File": "cursor.png", "HotspotX": 0, "HotspotY": 0 }, + * "TextInput": { "File": "cursor_text.png", "HotspotX": 8, "HotspotY": 12 } + * } + * }, * * // Simple color palette (6 key colors) * "Palette": { diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index 6fcbaef976..896f7dfe97 100644 --- a/src/Utils/FileSystem.cpp +++ b/src/Utils/FileSystem.cpp @@ -57,6 +57,11 @@ namespace Util return GetInterfacePath() / "Icons"; } + std::filesystem::path GetCursorsPath() + { + return GetInterfacePath() / "Cursors"; + } + std::filesystem::path GetSettingsUserPath() { return GetCommunityShaderPath() / "SettingsUser.json"; diff --git a/src/Utils/FileSystem.h b/src/Utils/FileSystem.h index ec1cfd194c..9adf9d67d7 100644 --- a/src/Utils/FileSystem.h +++ b/src/Utils/FileSystem.h @@ -61,6 +61,12 @@ namespace Util */ std::filesystem::path GetIconsPath(); + /** + * Gets the CommunityShaders Cursors directory path + * @return Interface / "Cursors" + */ + std::filesystem::path GetCursorsPath(); + /** * Gets the SettingsUser.json file path * @return CommunityShaderPath / "SettingsUser.json" From e0c890c59c90fd85837cde41ea89fb52ed8cc921 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:23:53 +0100 Subject: [PATCH 35/55] feat(skysync): rework shadow fader, expose sun/moon to shared data (#2408) --- .../Sky Sync/Shaders/Features/SkySync.ini | 2 +- .../CommunityShaders/Translations/en.json | 10 - .../CommunityShaders/Translations/zh_CN.json | 10 - package/Shaders/Common/SharedData.hlsli | 6 + src/Features/SkySync.cpp | 486 ++++++++---------- src/Features/SkySync.h | 103 +--- src/Hooks.cpp | 10 + src/Hooks.h | 6 + src/State.cpp | 34 ++ src/State.h | 9 + src/Utils/Moon.h | 91 ++++ 11 files changed, 398 insertions(+), 369 deletions(-) create mode 100644 src/Utils/Moon.h diff --git a/features/Sky Sync/Shaders/Features/SkySync.ini b/features/Sky Sync/Shaders/Features/SkySync.ini index 5dd39c9cbd..efa84cac75 100644 --- a/features/Sky Sync/Shaders/Features/SkySync.ini +++ b/features/Sky Sync/Shaders/Features/SkySync.ini @@ -1,2 +1,2 @@ [Info] -Version = 1-1-0 +Version = 1-2-0 diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index e4881b7930..ddfafe0519 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1186,16 +1186,6 @@ "feature.sky_sync.sun_path_southern": "Southern Sky", "feature.sky_sync.sun_path_tooltip": "Choose the trajectory the sun takes across the sky.", "feature.sky_sync.sun_path_vanilla": "Vanilla", - "feature.sky_sync.sun_position_offsets": "Sun Position Offsets", - "feature.sky_sync.sun_position_offsets_desc": "Moves sun height during sunrise/sunset. Reset weather to see changes.", - "feature.sky_sync.sunrise_begin": "Sunrise Begin (Hours)", - "feature.sky_sync.sunrise_begin_tooltip": "Offset for when the sun starts rising.", - "feature.sky_sync.sunrise_end": "Sunrise End (Hours)", - "feature.sky_sync.sunrise_end_tooltip": "Offset for when the sun finishes rising.", - "feature.sky_sync.sunset_begin": "Sunset Begin (Hours)", - "feature.sky_sync.sunset_begin_tooltip": "Offset for when the sun starts setting.", - "feature.sky_sync.sunset_end": "Sunset End (Hours)", - "feature.sky_sync.sunset_end_tooltip": "Offset for when the sun finishes setting.", "feature.sky_sync.use_alternate_sun_path": "Use alternate sun path", "feature.sky_sync.use_alternate_sun_path_tooltip": "Calculate sun position based on time of day and season instead of vanilla movement.", "feature.skylighting.description": "Simulates realistic ambient lighting by calculating sky occlusion and directional lighting, providing more accurate and natural illumination in outdoor environments.", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 6382ae69af..c80ce9dabd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -1185,16 +1185,6 @@ "feature.sky_sync.sun_path_southern": "南侧天空", "feature.sky_sync.sun_path_tooltip": "选择太阳穿越天空的轨迹。", "feature.sky_sync.sun_path_vanilla": "原版", - "feature.sky_sync.sun_position_offsets": "太阳位置偏移", - "feature.sky_sync.sun_position_offsets_desc": "在日出/日落时移动太阳高度。重置天气以查看更改。", - "feature.sky_sync.sunrise_begin": "日出开始(小时)", - "feature.sky_sync.sunrise_begin_tooltip": "太阳开始升起的时间偏移。", - "feature.sky_sync.sunrise_end": "日出结束(小时)", - "feature.sky_sync.sunrise_end_tooltip": "太阳完成升起的时间偏移。", - "feature.sky_sync.sunset_begin": "日落开始(小时)", - "feature.sky_sync.sunset_begin_tooltip": "太阳开始落下的时间偏移。", - "feature.sky_sync.sunset_end": "日落结束(小时)", - "feature.sky_sync.sunset_end_tooltip": "太阳完成落下的时间偏移。", "feature.sky_sync.use_alternate_sun_path": "使用备用太阳路径", "feature.sky_sync.use_alternate_sun_path_tooltip": "根据时间和季节计算太阳位置,而非原版运动。", "feature.skylighting.description": "通过计算天空遮蔽和方向光照,模拟逼真的环境照明,在户外环境中提供更精确自然的照明。", diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index e6ec03706b..c848328844 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -15,6 +15,12 @@ namespace SharedData row_major float3x4 DirectionalAmbient; float4 DirLightDirection; float4 DirLightColor; + float4 SunDirection; + float4 SunColor; + float4 MasserDirection; + float4 MasserColor; + float4 SecundaDirection; + float4 SecundaColor; float4 CameraData; float4 BufferDim; float Timer; diff --git a/src/Features/SkySync.cpp b/src/Features/SkySync.cpp index e2603401a5..579611fde8 100644 --- a/src/Features/SkySync.cpp +++ b/src/Features/SkySync.cpp @@ -10,11 +10,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( MoonLightSource, SunPath, CustomAngle, - SunriseBeginOffset, - SunriseEndOffset, - SunsetBeginOffset, - SunsetEndOffset, - MinShadowElevation) + MinShadowElevation, + ShadowTransitionDuration, + DimSunlightUnderHorizon, + NewMoonIntensity, + CrescentMoonIntensity, + FullMoonIntensity) void SkySync::DrawSettings() { @@ -65,26 +66,64 @@ void SkySync::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", T(TKEY("min_shadow_elevation_tooltip"), "The minimum angle sunlight will set to. Caps shadow length. Higher = shorter shadows at sunset/sunrise.")); } - ImGui::Spacing(); - ImGui::Spacing(); - if (ImGui::TreeNodeEx(T(TKEY("sun_position_offsets"), "Sun Position Offsets"), ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("%s", T(TKEY("sun_position_offsets_desc"), "Moves sun height during sunrise/sunset. Reset weather to see changes.")); - ImGui::SliderFloat(T(TKEY("sunrise_begin"), "Sunrise Begin (Hours)"), &settings.SunriseBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("sunrise_begin_tooltip"), "Offset for when the sun starts rising.")); - } - ImGui::SliderFloat(T(TKEY("sunrise_end"), "Sunrise End (Hours)"), &settings.SunriseEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("sunrise_end_tooltip"), "Offset for when the sun finishes rising.")); - } - ImGui::SliderFloat(T(TKEY("sunset_begin"), "Sunset Begin (Hours)"), &settings.SunsetBeginOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("sunset_begin_tooltip"), "Offset for when the sun starts setting.")); - } - ImGui::SliderFloat(T(TKEY("sunset_end"), "Sunset End (Hours)"), &settings.SunsetEndOffset, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_AlwaysClamp); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("sunset_end_tooltip"), "Offset for when the sun finishes setting.")); + + ImGui::SliderFloat("Shadow Transition Duration", &settings.ShadowTransitionDuration, 0.0f, 500.0f, "%.0f", ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How long (in game-time units) the shadow direction takes to fade between sources. 100 = ~5 seconds at timescale 20."); + } + + ImGui::Checkbox("Dim Sunlight Under Horizon", &settings.DimSunlightUnderHorizon); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted("Fade directional light to zero as the sun goes below the horizon."); + } + + ImGui::SliderFloat("New Moon Intensity", &settings.NewMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Crescent Intensity", &settings.CrescentMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + ImGui::SliderFloat("Full Moon Intensity", &settings.FullMoonIntensity, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_AlwaysClamp); + + if (ImGui::TreeNodeEx("Debug", ImGuiTreeNodeFlags_None)) { + static constexpr const char* CasterNames[] = { "Sun", "Masser", "Secunda", "None" }; + static constexpr const char* PhaseNames[] = { "Full", "Waning Gibbous", "Waning Quarter", "Waning Crescent", "New", "Waxing Crescent", "Waxing Quarter", "Waxing Gibbous" }; + + auto getPhase = [](const RE::Moon* moon) -> const char* { + if (!moon || !moon->moonMesh) + return "Unknown"; + if (const auto prop = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get())) { + if (auto tex = prop->GetBaseTexture()) + return PhaseNames[static_cast(Util::Moon::GetPhaseFromTexture(tex->name.c_str()))]; + } + return "Unknown"; + }; + + auto drawMoonEntry = [&](const char* label, Caster caster, const char* phase) { + auto& color = colors[static_cast(caster)]; + ImVec4 swatch = { color.x, color.y, color.z, 1.0f }; + ImGui::ColorButton(label, swatch, ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoPicker, { ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight() }); + ImGui::SameLine(); + ImGui::Text("%s [%s] color (%.3f, %.3f, %.3f, %.3f)", label, phase, color.x, color.y, color.z, color.w); + }; + + const auto sky = globals::game::sky; + drawMoonEntry("Masser", Caster::Masser, sky ? getPhase(sky->masser) : "Unknown"); + drawMoonEntry("Secunda", Caster::Secunda, sky ? getPhase(sky->secunda) : "Unknown"); + + ImGui::Text("Dim: %.3f", currentDim); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::Text("Shadow target: %s", CasterNames[static_cast(shadowFader.target)]); + ImGui::Text("Shadow dir: (%.2f, %.2f, %.2f)", shadowFader.currentDir.x, shadowFader.currentDir.y, shadowFader.currentDir.z); + if (shadowFader.transitioning) { + const float t = settings.ShadowTransitionDuration > 0.0f ? shadowFader.fadeTimer / settings.ShadowTransitionDuration : 1.0f; + ImGui::ProgressBar(t, { -1.0f, 0.0f }, ""); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Transitioning %.0f%%", t * 100.0f); + } else { + ImGui::TextDisabled("No transition"); } + ImGui::TreePop(); } } @@ -95,10 +134,6 @@ void SkySync::LoadSettings(json& o_json) settings.MoonLightSource = std::clamp(settings.MoonLightSource, static_cast(MoonLightSource::Brightest), static_cast(MoonLightSource::Secunda)); settings.SunPath = std::clamp(settings.SunPath, static_cast(SunPath::Southern), static_cast(SunPath::Custom)); settings.CustomAngle = std::clamp(settings.CustomAngle, -90.0f, 90.0f); - settings.SunriseBeginOffset = std::clamp(settings.SunriseBeginOffset, -5.0f, 5.0f); - settings.SunriseEndOffset = std::clamp(settings.SunriseEndOffset, -5.0f, 5.0f); - settings.SunsetBeginOffset = std::clamp(settings.SunsetBeginOffset, -5.0f, 5.0f); - settings.SunsetEndOffset = std::clamp(settings.SunsetEndOffset, -5.0f, 5.0f); settings.MinShadowElevation = std::clamp(settings.MinShadowElevation, 0.0f, 45.0f); SetSunAngle(); } @@ -125,14 +160,9 @@ void SkySync::PostPostLoad() return; } - stl::detour_thunk(REL::RelocationID(25626, 26169)); stl::detour_thunk(REL::RelocationID(25682, 26229)); - stl::detour_thunk(REL::RelocationID(25695, 26242)); gSunPosition = reinterpret_cast(REL::RelocationID(527924, 414871).address()); - gSunGlareSize = reinterpret_cast(REL::RelocationID(502611, 370235).address()); - gMasserSize = reinterpret_cast(REL::RelocationID(502558, 370155).address()); - gSecundaSize = reinterpret_cast(REL::RelocationID(502570, 370173).address()); logger::info("[Sky Sync] Installed hooks"); } @@ -152,6 +182,19 @@ void SkySync::DisableOnConflict(std::string_view conflictName) logger::warn("[Sky Sync] {}", failedLoadedMessage); } +void SkySync::OnSkyUpdateColors(RE::Sky* sky) +{ + if (!settings.Enabled || !sky || !settings.DimSunlightUnderHorizon) + return; + + if (currentDim > 0.0f && currentDim < 1.0f) { + auto& dirLight = sky->skyColor[static_cast(RE::TESWeather::ColorTypes::kSunlight)]; + dirLight.red *= currentDim; + dirLight.green *= currentDim; + dirLight.blue *= currentDim; + } +} + void SkySync::Sky_Update::thunk(RE::Sky* sky) { func(sky); @@ -160,14 +203,18 @@ void SkySync::Sky_Update::thunk(RE::Sky* sky) void SkySync::Update(const RE::Sky* sky) { - if (!settings.Enabled) + if (!settings.Enabled) { + currentDim = 1.0f; return; + } const auto sun = sky->sun; const auto climate = sky->currentClimate; const auto player = RE::PlayerCharacter::GetSingleton(); - if (!sun || !climate || !player) + if (!sun || !climate || !player) { + currentDim = 1.0f; return; + } const auto cell = player->GetParentCell(); @@ -181,20 +228,44 @@ void SkySync::Update(const RE::Sky* sky) // Exterior worldspaces always run; interior cells require the sunlight-shadows flag. if (cell && cell->IsInteriorCell() && !cell->cellFlags.all(static_cast(CellFlagExt::kSunlightShadows))) { + currentDim = 1.0f; return; } - const float time = sky->currentGameHour; - const bool isDayTime = time > timings.sunriseFadeOutMoonEnd && time < timings.sunsetFadeInMoonStart; + // Compute dim once per frame — used by OnSkyUpdateColors (if option on) and ShadowFader (always) + if (sky->currentClimate) { + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + const float sunriseBegin = timing.sunrise.begin / 6.0f; + const float sunriseMiddle = (timing.sunrise.begin + timing.sunrise.end) / 12.0f; + const float sunsetMiddle = (timing.sunset.begin + timing.sunset.end) / 12.0f; + const float sunsetEnd = timing.sunset.end / 6.0f; + + if (hour >= sunsetMiddle && hour < sunsetEnd) { + float range = sunsetEnd - sunsetMiddle; + float t = range > 0.0f ? (hour - sunsetMiddle) / range : 1.0f; + currentDim = std::sqrt(1.0f - t); + } else if (hour >= sunsetEnd || hour < sunriseBegin) { + currentDim = 0.0f; + } else if (hour >= sunriseBegin && hour < sunriseMiddle) { + float range = sunriseMiddle - sunriseBegin; + float t = range > 0.0f ? (hour - sunriseBegin) / range : 1.0f; + currentDim = std::sqrt(t); + } else { + currentDim = 1.0f; + } + } else { + currentDim = 1.0f; + } - const auto worldSpace = player->GetWorldspace(); - const float altitude = worldSpace ? player->GetPositionZ() - worldSpace->GetDefaultWaterHeight() : 0.0f; + RE::NiPoint3 directions[3] = {}; + float intensities[3] = {}; - ProcessSun(sun, time, altitude, isDayTime); - ProcessMoon(sky->masser, time, Caster::Masser, altitude, isDayTime); - ProcessMoon(sky->secunda, time, Caster::Secunda, altitude, isDayTime); + ProcessSun(sky, directions, intensities); + ProcessMoon(sky, Caster::Masser, directions, intensities); + ProcessMoon(sky, Caster::Secunda, directions, intensities); - shadowFader.Update(sun, directions, intensities, isDayTime); + shadowFader.Update(sky, directions, intensities, settings.ShadowTransitionDuration); } void SkySync::SetSunAngle() { @@ -232,73 +303,72 @@ void SkySync::SetSkyRotation(const RE::Sky* sky, RE::TESObjectCELL* cell) sky->root->Update(updateData); } -void SkySync::ProcessSun(const RE::Sun* sun, const float time, const float altitude, const bool isDayTime) +void SkySync::ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[]) { + const auto sun = sky->sun; RE::NiPoint3 dir; float dist; if (settings.UseAlternateSunPath) { - CalculateAlternateSunDirectionAndDistance(dir, dist, time, timings.sunrise, timings.sunset, sunAngle); + const auto climate = sky->currentClimate; + const float sunrise = (climate->timing.sunrise.begin / 6.0f + climate->timing.sunrise.end / 6.0f) * 0.5f - 0.25f; + const float sunset = (climate->timing.sunset.begin / 6.0f + climate->timing.sunset.end / 6.0f) * 0.5f + 0.25f; + CalculateAlternateSunDirectionAndDistance(dir, dist, sky->currentGameHour, sunrise, sunset, sunAngle); } else CalculateSunDirectionAndDistance(sun, dir, dist); - rawDirections[static_cast(Caster::Sun)] = dir; - - const RE::NiPoint3 apparentDir = GetApparentDirection(dir, altitude); - SetSunPosition(sun, apparentDir, dist); + SetSunPosition(sun, dir, dist); - directions[static_cast(Caster::Sun)] = apparentDir; + dirs[static_cast(Caster::Sun)] = dir; - SetSunBaseVisibility(sun, isDayTime ? 1.0f : 0.0f); - - intensities[static_cast(Caster::Sun)] = isDayTime ? CalculateVisibility(dir, dist, *gSunGlareSize * SunScaleFactor) : 0.0f; + if (const auto prop = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) + intensities[static_cast(Caster::Sun)] = prop->kBlendColor.alpha; } -void SkySync::ProcessMoon(const RE::Moon* moon, const float time, const Caster type, const float altitude, const bool isDayTime) +void SkySync::ProcessMoon(const RE::Sky* sky, const Caster type, RE::NiPoint3 dirs[], float intensities[]) { - intensities[static_cast(type)] = 0.0f; - directions[static_cast(type)] = { 0.0f, 0.0f, 1.0f }; - rawDirections[static_cast(type)] = { 0.0f, 0.0f, -1.0f }; + const int idx = static_cast(type); + colors[idx] = {}; - if (!moon) + const auto moon = type == Caster::Masser ? sky->masser : sky->secunda; + if (!moon || moon->root->GetFlags().any(RE::NiAVObject::Flag::kHidden)) return; - const auto dir = moon->root->local.rotate.GetVectorY(); - - rawDirections[static_cast(type)] = dir; + auto dir = moon->root->local.rotate.GetVectorY(); - auto apparentDir = GetApparentDirection(dir, altitude); - SetMoonDirection(moon, apparentDir); - - // Moon and Stars adjusts some intermediary rotation matrices for the moon - // Directly changing the directions here avoids 3 matrix multiplications and a vector rotation if (moonAndStarsLoaded) - apparentDir = { apparentDir.y, -apparentDir.x, apparentDir.z }; + dir = { dir.y, -dir.x, dir.z }; - directions[static_cast(type)] = apparentDir; + dirs[idx] = dir; - if (isDayTime) - return; + const float4& baseColor = type == Caster::Masser ? Util::Moon::MasserBaseColor : Util::Moon::SecundaBaseColor; + float4 color = Util::Moon::GetBlendColor(moon, baseColor, settings.NewMoonIntensity, settings.CrescentMoonIntensity, settings.FullMoonIntensity); + colors[idx] = color; const auto src = static_cast(settings.MoonLightSource); const bool isValidSource = src == MoonLightSource::Brightest || (src == MoonLightSource::Masser && type == Caster::Masser) || (src == MoonLightSource::Secunda && type == Caster::Secunda); if (!isValidSource) return; - const float moonRadius = type == Caster::Masser ? static_cast(*gMasserSize) : static_cast(*gSecundaSize); - float intensity = CalculateVisibility(dir, moon->moonMesh->local.translate.y, moonRadius); - - if (type == Caster::Masser) - intensity *= masserPhaseIntensityFactor; - else if (type == Caster::Secunda) - intensity *= secundaPhaseIntensityFactor * SecundaIntensityFactor; + intensities[idx] = color.w; +} - if (time >= timings.sunriseFadeOutMoonStart && time <= timings.sunriseFadeOutMoonEnd) - intensity *= SmoothStep(timings.sunriseFadeOutMoonEnd, timings.sunriseFadeOutMoonStart, time); - else if (time >= timings.sunsetFadeInMoonStart && time <= timings.sunsetFadeInMoonEnd) - intensity *= SmoothStep(timings.sunsetFadeInMoonStart, timings.sunsetFadeInMoonEnd, time); +bool SkySync::IsNight(const RE::Sky* sky) +{ + if (!sky || !sky->currentClimate) + return false; + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + return hour >= timing.sunset.end / 6.0f || hour < timing.sunrise.begin / 6.0f; +} - intensities[static_cast(type)] = intensity; +bool SkySync::IsDaytime(const RE::Sky* sky) +{ + if (!sky || !sky->currentClimate) + return false; + const auto& timing = sky->currentClimate->timing; + const float hour = sky->currentGameHour; + return hour >= timing.sunrise.end / 6.0f && hour < timing.sunset.begin / 6.0f; } inline void SkySync::CalculateSunDirectionAndDistance(const RE::Sun* sun, RE::NiPoint3& outDir, float& outDistance) @@ -329,26 +399,6 @@ inline void SkySync::CalculateAlternateSunDirectionAndDistance(RE::NiPoint3& out outDist = std::lerp(SunHorizonDistance, SunPeakDistance, elevationRatio); } -RE::NiPoint3 SkySync::GetApparentDirection(const RE::NiPoint3& dir, const float altitude) -{ - const float dipAngle = -std::atan(altitude / RenderDistance); - float sinPhi, cosPhi; - DirectX::XMScalarSinCosEst(&sinPhi, &cosPhi, dipAngle); - - const auto rotationAxis = dir.UnitCross({ 0.0f, 0.0f, 1.0f }); - const float axisDotDir = rotationAxis.Dot(dir); - const auto axisCrossDir = rotationAxis.Cross(dir); - const float oneMinusCosPhi = 1.0f - cosPhi; - - const float x = dir.x * cosPhi + axisCrossDir.x * sinPhi + rotationAxis.x * (axisDotDir * oneMinusCosPhi); - const float y = dir.y * cosPhi + axisCrossDir.y * sinPhi + rotationAxis.y * (axisDotDir * oneMinusCosPhi); - const float z = dir.z * cosPhi + axisCrossDir.z * sinPhi + rotationAxis.z * (axisDotDir * oneMinusCosPhi); - - RE::NiPoint3 rotated = { x, y, z }; - rotated.Unitize(); - return rotated; -} - inline void SkySync::SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, const float distance) { const auto position = dir * distance; @@ -357,115 +407,99 @@ inline void SkySync::SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, *gSunPosition = position; } -inline void SkySync::SetMoonDirection(const RE::Moon* moon, const RE::NiPoint3& dir) +void SkySync::ShadowFader::Reset() { - auto& m = moon->root->local.rotate; - m.entry[0][1] = dir.x; - m.entry[1][1] = dir.y; - m.entry[2][1] = dir.z; + target = Caster::Sun; + previousTarget = Caster::Sun; + fadeTimer = 0.0f; + transitioning = false; } -inline float SkySync::CalculateVisibility(const RE::NiPoint3& dir, const float dist, const float radius) +void SkySync::ShadowFader::Update(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[], float fadeDuration) { - const float height = dir.Dot({ 0.0f, 0.0f, 1.0f }) * dist; - return SmoothStep(-radius, radius, height); -} + auto isValidDir = [](const RE::NiPoint3& d) { return d.x != 0.0f || d.y != 0.0f || d.z != 0.0f; }; -inline void SkySync::SetSunBaseVisibility(const RE::Sun* sun, const float visibility) -{ - if (const auto property = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) - property->kBlendColor.alpha = visibility; -} + Caster best; -void SkySync::ShadowFader::Reset() -{ - fadePhase = Phase::None; - current = Caster::None; - target = Caster::None; - fadeTimer = 0.0f; -} + if (globals::features::skySync.currentDim <= 0.0f) { + bool masserValid = isValidDir(dirs[static_cast(Caster::Masser)]); + bool secundaValid = isValidDir(dirs[static_cast(Caster::Secunda)]); -void SkySync::ShadowFader::Update(const RE::Sun* sun, RE::NiPoint3 dirs[3], float intensities[3], const bool isDayTime) -{ - const float masserIntensity = intensities[static_cast(Caster::Masser)]; - const float secundaIntensity = intensities[static_cast(Caster::Secunda)]; - - auto desired = Caster::None; - if (isDayTime) - desired = Caster::Sun; - else if (masserIntensity > 0.0f && masserIntensity >= secundaIntensity) - desired = Caster::Masser; - else if (secundaIntensity > 0.0f) - desired = Caster::Secunda; - - if (desired != target) { - target = desired; - fadeTimer = 0.0f; + if (!masserValid && !secundaValid) { + // No valid night caster — default to directly above (shadows point down) + currentDir = { 0.0f, 0.0f, 1.0f }; + SetLighting(sky, currentDir); + return; + } - if (current == Caster::None) { - fadePhase = Phase::FadeIn; - current = target; - } else - fadePhase = Phase::FadeOut; + if (!masserValid) + best = Caster::Secunda; + else if (!secundaValid || intensities[static_cast(Caster::Secunda)] <= intensities[static_cast(Caster::Masser)]) + best = Caster::Masser; + else + best = Caster::Secunda; + } else { + best = Caster::Sun; } - float timeScale = 20.0f; - if (const auto calendar = globals::game::calendar) { - const float currentHoursPassed = calendar->GetHoursPassed(); - timeScale = calendar->GetTimescale(); - const float hoursPassedDiff = std::abs(currentHoursPassed - previousHoursPassed); - previousHoursPassed = currentHoursPassed; - if (timeScale <= 0.0f || hoursPassedDiff >= 0.01f) { - fadePhase = Phase::None; - current = target; + // If best source changed, begin a new transition + if (best != target) { + previousTarget = target; + target = best; + startDir = currentDir; + fadeTimer = 0.0f; + transitioning = true; + + // Snap instantly if transitioning to sun during daytime or to moon during full night + bool snap = (best == Caster::Sun && IsDaytime(sky)) || + ((best == Caster::Masser || best == Caster::Secunda) && IsNight(sky)); + if (snap) { + transitioning = false; + currentDir = dirs[static_cast(best)]; + SetLighting(sky, currentDir); + return; } } - if (current == Caster::None) { - fadePhase = Phase::None; - SetLighting(sun, { 0.0f, 0.0f, 1.0f }, 0.0f); + if (!transitioning) { + currentDir = dirs[static_cast(target)]; + SetLighting(sky, currentDir); return; } - const auto& dir = dirs[static_cast(current)]; - const auto intensity = intensities[static_cast(current)]; + float timeScale = 20.0f; + if (const auto calendar = globals::game::calendar) + timeScale = calendar->GetTimescale(); + fadeTimer = std::min(fadeTimer + *globals::game::deltaTime * 20.0f / timeScale, fadeDuration); + const float t = fadeDuration > 0.0f ? fadeTimer / fadeDuration : 1.0f; + + RE::NiPoint3 targetDir = dirs[static_cast(target)]; + currentDir = { + std::lerp(startDir.x, targetDir.x, t), + std::lerp(startDir.y, targetDir.y, t), + std::lerp(startDir.z, targetDir.z, t) + }; + currentDir.Unitize(); - if (fadePhase == Phase::None) { - SetLighting(sun, dir, intensity); - return; + if (t >= 1.0f) { + currentDir = targetDir; + transitioning = false; } - fadeTimer = std::min(fadeTimer + *globals::game::deltaTime * timeScale, FadeTime); - - const float t = fadeTimer / FadeTime; - const float fade = fadePhase == Phase::FadeIn ? t : 1.0f - t; - SetLighting(sun, dir, intensity * fade); - - if (fadePhase == Phase::FadeOut) { - if (t >= 1.0f || intensity <= 0.0f) { - current = target; - fadePhase = Phase::FadeIn; - fadeTimer = 0.0f; - } - } else if (fadePhase == Phase::FadeIn) { - if (t >= 1.0f) - fadePhase = Phase::None; - } + SetLighting(sky, currentDir); } -void SkySync::ShadowFader::SetLighting(const RE::Sun* sun, RE::NiPoint3 dir, float intensity) +void SkySync::ShadowFader::SetLighting(const RE::Sky* sky, RE::NiPoint3 dir) { ClampDirection(dir); - RE::NiMatrix3& m = sun->light->local.rotate; + RE::NiMatrix3& m = sky->sun->light->local.rotate; m.entry[0][0] = -dir.x; m.entry[1][0] = -dir.y; m.entry[2][0] = -dir.z; RE::NiUpdateData updateData; - sun->light->Update(updateData); - - intensity = std::clamp(intensity, 0.0f, 1.0f); + sky->sun->light->Update(updateData); } inline void SkySync::ShadowFader::ClampDirection(RE::NiPoint3& dir) @@ -486,94 +520,6 @@ inline void SkySync::ShadowFader::ClampDirection(RE::NiPoint3& dir) dir.z = sinElev; } -void SkySync::ClimateTimings::Update(const RE::TESClimate* climate) -{ - const float SunriseBeginOffset = globals::features::skySync.settings.SunriseBeginOffset; - const float SunriseEndOffset = globals::features::skySync.settings.SunriseEndOffset; - const float SunsetBeginOffset = globals::features::skySync.settings.SunsetBeginOffset; - const float SunsetEndOffset = globals::features::skySync.settings.SunsetEndOffset; - - sunriseBegin = (climate->timing.sunrise.begin / 6.0f) + SunriseBeginOffset; - sunriseEnd = (climate->timing.sunrise.end / 6.0f) + SunriseEndOffset; - sunsetBegin = (climate->timing.sunset.begin / 6.0f) + SunsetBeginOffset; - sunsetEnd = (climate->timing.sunset.end / 6.0f) + SunsetEndOffset; - // Basic ordering guarantees (prevents divide-by-zero / negative duration paths). - constexpr float kMinGapHours = 0.1f; - if (sunriseEnd <= sunriseBegin) - sunriseEnd = sunriseBegin + kMinGapHours; - if (sunsetEnd <= sunsetBegin) - sunsetEnd = sunsetBegin + kMinGapHours; - if (sunsetBegin <= sunriseEnd) - sunsetBegin = sunriseEnd + kMinGapHours; - if (sunsetEnd <= sunsetBegin) - sunsetEnd = sunsetBegin + kMinGapHours; - sunrise = (sunriseBegin + sunriseEnd) * 0.5f - 0.25f; - sunset = (sunsetBegin + sunsetEnd) * 0.5f + 0.25f; - sunriseFadeOutMoonStart = sunriseBegin - 0.5f; - sunriseFadeOutMoonEnd = sunriseBegin + 1.0f; - sunsetFadeInMoonStart = sunsetEnd - 1.0f; - sunsetFadeInMoonEnd = sunsetEnd + 0.5f; -} - -void SkySync::Sky_OnNewClimate::thunk(RE::Sky* sky) -{ - if (auto& singleton = globals::features::skySync; singleton.settings.Enabled && sky && sky->currentClimate) - singleton.timings.Update(sky->currentClimate); - func(sky); -} - -void SkySync::Moon_Update::thunk(RE::Moon* moon, RE::Sky* sky) -{ - const auto updateMoonTexture = moon->updateMoonTexture; - - func(moon, sky); - - if (auto& singleton = globals::features::skySync; singleton.settings.Enabled && updateMoonTexture != moon->updateMoonTexture) { - // Gets the texture name of the current moon phase when it changes rather than reading direct global variables - // Allows for compatability with other mods that don't directly update the in-game phase values - const auto moonShaderProperty = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get()); - - const auto name = moonShaderProperty->GetBaseTexture()->name.c_str(); - const size_t len = std::strlen(name); - std::string lower; - lower.reserve(len); - for (size_t i = 0; i < len; ++i) { - lower.push_back(static_cast(std::tolower(name[i]))); - } - static constexpr std::array, 8> Lookup{ - { { "full", RE::Moon::Phases::Phase::kFull }, - { "three_wan", RE::Moon::Phases::Phase::kWaningGibbous }, - { "half_wan", RE::Moon::Phases::Phase::kWaningQuarter }, - { "one_wan", RE::Moon::Phases::Phase::kWaningCrescent }, - { "new", RE::Moon::Phases::Phase::kNewMoon }, - { "one_wax", RE::Moon::Phases::Phase::kWaxingCrescent }, - { "half_wax", RE::Moon::Phases::Phase::kWaxingQuarter }, - { "three_wax", RE::Moon::Phases::Phase::kWaxingGibbous } } - }; - - RE::Moon::Phases::Phase phase = RE::Moon::Phases::Phase::kFull; - for (auto& [suffix, id] : Lookup) { - if (lower.find(suffix) != std::string::npos) { - phase = id; - break; - } - } - - float* intensityFactor = moon == sky->masser ? &singleton.masserPhaseIntensityFactor : &singleton.secundaPhaseIntensityFactor; - if (phase == RE::Moon::Phases::Phase::kNewMoon) { - *intensityFactor = NewMoonIntensityFactor; - } else { - const float t = (abs(static_cast(phase) - static_cast(RE::Moon::Phases::Phase::kNewMoon)) - 1.0f) / 3.0f; - *intensityFactor = std::lerp(CrescentMoonIntensityFactor, FullMoonIntensityFactor, t); - } - } -} - -inline float SkySync::SmoothStep(const float start, const float end, const float x) -{ - const float t = std::clamp((x - start) / (end - start), 0.0f, 1.0f); - return t * t * (3.0f - 2.0f * t); -} #undef I18N_KEY_PREFIX diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 651b987f1f..2290fe1819 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -1,6 +1,8 @@ #pragma once #include "RE/M/Moon.h" +#include "Utils/Moon.h" + struct SkySync : Feature { private: @@ -27,15 +29,16 @@ struct SkySync : Feature struct Settings { bool Enabled = true; - bool UseAlternateSunPath = true; + bool UseAlternateSunPath = false; int32_t MoonLightSource = 0; int32_t SunPath = 0; float CustomAngle = -35.0f; - float SunriseBeginOffset = 0.0f; - float SunriseEndOffset = 0.0f; - float SunsetBeginOffset = 0.0f; - float SunsetEndOffset = 0.0f; - float MinShadowElevation = 0.25f; + float MinShadowElevation = 18.0f; + float ShadowTransitionDuration = 100.0f; + bool DimSunlightUnderHorizon = true; + float NewMoonIntensity = 0.05f; + float CrescentMoonIntensity = 0.25f; + float FullMoonIntensity = 1.0f; }; Settings settings; @@ -49,6 +52,8 @@ struct SkySync : Feature virtual bool IsCore() const override { return true; } virtual bool SupportsVR() override { return true; } + void OnSkyUpdateColors(RE::Sky* sky); + virtual void PostPostLoad() override; virtual void DataLoaded() override; @@ -58,17 +63,6 @@ struct SkySync : Feature static inline REL::Relocation func; }; - struct Sky_OnNewClimate - { - static void thunk(RE::Sky* sky); - static inline REL::Relocation func; - }; - - struct Moon_Update - { - static void thunk(RE::Moon* moon, RE::Sky* sky); - static inline REL::Relocation func; - }; private: enum class CellFlagExt : uint16_t @@ -104,76 +98,36 @@ struct SkySync : Feature const char* MoonLightSourceNames[static_cast(MoonLightSource::Count)] = { "Brightest", "Masser", "Secunda" }; const char* SunPathNames[static_cast(SunPath::Count)] = { "Southern Sky", "Northern Sky", "Vanilla", "Custom" }; - struct ClimateTimings - { - float sunriseFadeOutMoonStart; - float sunriseBegin; - float sunriseFadeOutMoonEnd; - float sunrise; - float sunriseEnd; - float sunsetBegin; - float sunset; - float sunsetFadeInMoonStart; - float sunsetEnd; - float sunsetFadeInMoonEnd; - - void Update(const RE::TESClimate* climate); - }; - struct ShadowFader { - enum class Phase : uint8_t - { - None, - FadeOut, - FadeIn - }; - - static constexpr float FadeTime = 100.0f; // 5 seconds at timescale 20 - - Phase fadePhase = Phase::None; - Caster current = Caster::None; - Caster target = Caster::None; + RE::NiPoint3 currentDir = { 0.0f, 0.0f, 1.0f }; + RE::NiPoint3 startDir = { 0.0f, 0.0f, 1.0f }; + Caster target = Caster::Sun; + Caster previousTarget = Caster::Sun; float fadeTimer = 0.0f; - float previousHoursPassed = 0.0f; + bool transitioning = false; - void Update(const RE::Sun* sun, RE::NiPoint3 dirs[], float intensities[], bool isDayTime); - static void SetLighting(const RE::Sun* sun, RE::NiPoint3 dir, float intensity); + void Update(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[], float fadeDuration); + static void SetLighting(const RE::Sky* sky, RE::NiPoint3 dir); static void ClampDirection(RE::NiPoint3& dir); void Reset(); }; - static constexpr float RenderDistance = 325000.0f; static constexpr float SunHorizonDistance = 280.0f; static constexpr float SunPeakDistance = 400.0f; - static constexpr float SunScaleFactor = 48.0f / 2048.0f; - static constexpr float SouthernSunAngle = 90.0f - 35.0f; static constexpr float NorthernSunAngle = 90.0f + 35.0f; static constexpr float VanillaSunAngle = 90.0f + 5.0f; - static constexpr float SecundaIntensityFactor = 0.67f; - static constexpr float NewMoonIntensityFactor = 0.05f; - static constexpr float CrescentMoonIntensityFactor = 0.25f; - static constexpr float FullMoonIntensityFactor = 1.0f; - inline static RE::NiPoint3* gSunPosition = nullptr; - inline static float* gSunGlareSize = nullptr; - inline static uint32_t* gMasserSize = nullptr; - inline static uint32_t* gSecundaSize = nullptr; bool moonAndStarsLoaded = false; RE::TESObjectCELL* currentCell = nullptr; float sunAngle = 90.0f; float currentSkyRotation = D3D11_FLOAT32_MAX; - float masserPhaseIntensityFactor = 0.0f; - float secundaPhaseIntensityFactor = 0.0f; - - ClimateTimings timings = {}; - RE::NiPoint3 rawDirections[3]; - RE::NiPoint3 directions[3]; - float intensities[3] = {}; + float4 colors[3] = {}; + float currentDim = 1.0f; ShadowFader shadowFader; void DisableOnConflict(std::string_view conflictName); @@ -184,23 +138,16 @@ struct SkySync : Feature void SetSkyRotation(const RE::Sky* sky, RE::TESObjectCELL* cell); - void ProcessSun(const RE::Sun* sun, float time, float altitude, bool isDayTime); + void ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[]); + + void ProcessMoon(const RE::Sky* sky, Caster type, RE::NiPoint3 dirs[], float intensities[]); - void ProcessMoon(const RE::Moon* moon, float time, Caster type, float altitude, bool isDayTime); + static bool IsNight(const RE::Sky* sky); + static bool IsDaytime(const RE::Sky* sky); static void CalculateSunDirectionAndDistance(const RE::Sun* sun, RE::NiPoint3& outDir, float& outDistance); static void CalculateAlternateSunDirectionAndDistance(RE::NiPoint3& outDir, float& outDist, float time, float sunrise, float sunset, float sunAngle); - static RE::NiPoint3 GetApparentDirection(const RE::NiPoint3& dir, float altitude); - static void SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, float distance); - - static void SetMoonDirection(const RE::Moon* moon, const RE::NiPoint3& dir); - - static float CalculateVisibility(const RE::NiPoint3& dir, float dist, float radius); - - static void SetSunBaseVisibility(const RE::Sun* sun, float visibility); - - static float SmoothStep(float start, float end, float x); }; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 04272dd855..b11a89eca7 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -15,6 +15,7 @@ #include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" #include "Features/Skin.h" +#include "Features/SkySync.h" #include "Features/Upscaling.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" @@ -866,6 +867,12 @@ namespace Hooks static inline REL::Relocation func; }; + void Sky_UpdateColors::thunk(RE::Sky* sky, float a_delta) + { + func(sky, a_delta); + globals::features::skySync.OnSkyUpdateColors(sky); + } + /** * @brief Installs hooks, detours, and memory patches for graphics, input, and rendering subsystems. * @@ -935,6 +942,9 @@ namespace Hooks logger::info("Hooking TESWaterReflections::Update_Actor::GetLOSPosition for Sky Reflection Fix"); stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA, 0x1ed)); + logger::info("Hooking Sky::UpdateColors"); + stl::detour_thunk(REL::RelocationID(25686, 26233)); + logger::info("Installing SetupGeometry hooks"); stl::write_vfunc<0x6, EffectExtensions::BSEffectShader_SetupGeometry>(RE::VTABLE_BSEffectShader[0]); stl::write_vfunc<0x6, SkyExtensions::BSSkyShader_SetupGeometry>(RE::VTABLE_BSSkyShader[0]); diff --git a/src/Hooks.h b/src/Hooks.h index 335a7df7d8..b87fcc6c57 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -19,6 +19,12 @@ namespace Hooks static inline REL::Relocation func; }; + struct Sky_UpdateColors + { + static void thunk(RE::Sky* sky, float a_delta); + static inline REL::Relocation func; + }; + void Install(); void InstallEarlyHooks(); } diff --git a/src/State.cpp b/src/State.cpp index 6ddb97af5f..2a7a79e65d 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -13,6 +13,7 @@ #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" #include "Features/Skin.h" +#include "Features/SkySync.h" #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" @@ -218,6 +219,11 @@ void State::Reset() void State::Setup() { + // Detect Moon and Stars mod for compatibility adjustments + moonAndStarsLoaded = GetModuleHandle(L"po3_MoonMod.dll") != nullptr; + if (moonAndStarsLoaded) + logger::info("Moon and Stars detected, compatibility enabled"); + SetupResources(); // Probe typed UAV load support before features set up their resources, so any @@ -1032,6 +1038,34 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.MipBias = 0; } + if (auto sky = globals::game::sky) { + // Process sun + if (auto sun = sky->sun; sun && sun->root && sky->root) { + const auto& sunPos = sun->root->world.translate; + const auto& skyPos = sky->root->world.translate; + float3 sunDirection = { sunPos.x - skyPos.x, sunPos.y - skyPos.y, sunPos.z - skyPos.z }; + sunDirection.Normalize(); + data.SunDirection = { sunDirection.x, sunDirection.y, sunDirection.z, 0.0f }; + + if (sun->sunBase) { + if (const auto prop = skyrim_cast(sun->sunBase->GetGeometryRuntimeData().shaderProperty.get())) + data.SunColor = { prop->kBlendColor.red * prop->kBlendColor.alpha, prop->kBlendColor.green * prop->kBlendColor.alpha, prop->kBlendColor.blue * prop->kBlendColor.alpha, prop->kBlendColor.alpha }; + } + } + + if (auto masser = sky->masser) { + auto dir = Util::Moon::GetDirection(masser, moonAndStarsLoaded); + data.MasserDirection = { dir.x, dir.y, dir.z, 0.0f }; + data.MasserColor = Util::Moon::GetBlendColor(masser, Util::Moon::MasserBaseColor, globals::features::skySync.settings.NewMoonIntensity, globals::features::skySync.settings.CrescentMoonIntensity, globals::features::skySync.settings.FullMoonIntensity); + } + + if (auto secunda = sky->secunda) { + auto dir = Util::Moon::GetDirection(secunda, moonAndStarsLoaded); + data.SecundaDirection = { dir.x, dir.y, dir.z, 0.0f }; + data.SecundaColor = Util::Moon::GetBlendColor(secunda, Util::Moon::SecundaBaseColor, globals::features::skySync.settings.NewMoonIntensity, globals::features::skySync.settings.CrescentMoonIntensity, globals::features::skySync.settings.FullMoonIntensity); + } + } + // DALC to SH const auto& m = dalcTransform.rotate; const auto& t = dalcTransform.translate; diff --git a/src/State.h b/src/State.h index 2dd811d445..7057fee387 100644 --- a/src/State.h +++ b/src/State.h @@ -241,6 +241,12 @@ class State DirectX::XMFLOAT3X4 DirectionalAmbient; float4 DirLightDirection; float4 DirLightColor; + float4 SunDirection; + float4 SunColor; + float4 MasserDirection; + float4 MasserColor; + float4 SecundaDirection; + float4 SecundaColor; float4 CameraData; float4 BufferDim; float Timer; @@ -275,6 +281,9 @@ class State TracyD3D11Ctx tracyCtx = nullptr; // Tracy context + // Moon and Stars mod detection + inline static bool moonAndStarsLoaded = false; + void ClearDisabledFeatures(); bool SetFeatureDisabled(const std::string& featureName, bool isDisabled); bool IsFeatureDisabled(const std::string& featureName); diff --git a/src/Utils/Moon.h b/src/Utils/Moon.h new file mode 100644 index 0000000000..fab3f638df --- /dev/null +++ b/src/Utils/Moon.h @@ -0,0 +1,91 @@ +// Shared moon processing utilities +#pragma once + +namespace Util::Moon +{ + // Moon phase intensity constants + static constexpr float NewMoonIntensityFactor = 0.05f; + static constexpr float CrescentMoonIntensityFactor = 0.25f; + static constexpr float FullMoonIntensityFactor = 1.0f; + + // Moon base colors (RGB/255) + static constexpr float4 MasserBaseColor = { 142.0f / 255.0f, 96.0f / 255.0f, 90.0f / 255.0f, 1.0f }; + static constexpr float4 SecundaBaseColor = { 117.0f / 255.0f, 115.0f / 255.0f, 109.0f / 255.0f, 1.0f }; + + // Phase lookup table for determining moon phase from texture name + static constexpr std::array, 8> PhaseLookup{ + { { "full", RE::Moon::Phases::Phase::kFull }, + { "three_wan", RE::Moon::Phases::Phase::kWaningGibbous }, + { "half_wan", RE::Moon::Phases::Phase::kWaningQuarter }, + { "one_wan", RE::Moon::Phases::Phase::kWaningCrescent }, + { "new", RE::Moon::Phases::Phase::kNewMoon }, + { "one_wax", RE::Moon::Phases::Phase::kWaxingCrescent }, + { "half_wax", RE::Moon::Phases::Phase::kWaxingQuarter }, + { "three_wax", RE::Moon::Phases::Phase::kWaxingGibbous } } + }; + + inline float GetPhaseIntensityFactor(RE::Moon::Phases::Phase phase, float newMoon = NewMoonIntensityFactor, float crescent = CrescentMoonIntensityFactor, float full = FullMoonIntensityFactor) + { + if (phase == RE::Moon::Phases::Phase::kNewMoon) { + return newMoon; + } else { + const float t = (abs(static_cast(phase) - static_cast(RE::Moon::Phases::Phase::kNewMoon)) - 1.0f) / 3.0f; + return std::lerp(crescent, full, t); + } + } + + inline RE::Moon::Phases::Phase GetPhaseFromTexture(const char* textureName) + { + if (!textureName) + return RE::Moon::Phases::Phase::kFull; + + const size_t len = std::strlen(textureName); + std::string lower; + lower.reserve(len); + for (size_t i = 0; i < len; ++i) { + lower.push_back(static_cast(std::tolower(static_cast(textureName[i])))); + } + + for (auto& [suffix, id] : PhaseLookup) { + if (lower.find(suffix) != std::string::npos) { + return id; + } + } + + return RE::Moon::Phases::Phase::kFull; + } + + inline RE::NiPoint3 GetDirection(const RE::Moon* moon, bool applyMoonAndStarsCompat = false) + { + if (!moon || !moon->root) + return { 0.0f, 0.0f, 1.0f }; + + auto dir = moon->root->world.rotate.GetVectorY(); + dir.Unitize(); + + if (applyMoonAndStarsCompat) { + std::swap(dir.x, dir.y); + dir.x = -dir.x; + } + + return dir; + } + + inline float4 GetBlendColor(const RE::Moon* moon, const float4& baseColor, float newMoon = NewMoonIntensityFactor, float crescent = CrescentMoonIntensityFactor, float full = FullMoonIntensityFactor) + { + if (!moon || !moon->moonMesh) + return {}; + + const auto prop = skyrim_cast(moon->moonMesh->GetGeometryRuntimeData().shaderProperty.get()); + if (!prop) + return {}; + + float phase = 1.0f; + if (auto tex = prop->GetBaseTexture()) + phase = GetPhaseIntensityFactor(GetPhaseFromTexture(tex->name.c_str()), newMoon, crescent, full); + + float alpha = prop->kBlendColor.alpha; + return { prop->kBlendColor.red * baseColor.x * phase * alpha, prop->kBlendColor.green * baseColor.y * phase * alpha, prop->kBlendColor.blue * baseColor.z * phase * alpha, alpha }; + } + +} From e9ecea37169398ff7ba5e63ecd98a06a9b7406a2 Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:06:33 +0100 Subject: [PATCH 36/55] fix(lighting): create Masks2 RT and divide SSGI AO by vertexAO (#2411) --- package/Shaders/DeferredCompositeCS.hlsl | 6 ++++ package/Shaders/Lighting.hlsl | 37 +++++------------------- package/Shaders/RunGrass.hlsl | 16 +++++----- src/Deferred.cpp | 7 +++-- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index b3610417a6..cadbf90424 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -13,6 +13,7 @@ Texture2D SpecularTexture : register(t0); Texture2D AlbedoTexture : register(t1); Texture2D NormalRoughnessTexture : register(t2); Texture2D MasksTexture : register(t3); +Texture2D Masks2Texture : register(t9); RWTexture2D MainRW : register(u0); RWTexture2D NormalTAAMaskSpecularMaskRW : register(u1); @@ -139,6 +140,11 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float3 ssgiIl; SampleSSGI(dispatchID.xy, normalWS, ssgiAo, ssgiIl); + // Masks2.x stores 1 - vertexAO (Lighting.hlsl only); cleared to 0 for + // pixels with no vertex AO contribution, so vertexAO defaults to 1. + float vertexAO = 1.0 - Masks2Texture[dispatchID.xy].x; + ssgiAo = saturate(ssgiAo / max(vertexAO, EPSILON_DIVISION)); + float3 linAlbedo = Color::IrradianceToLinear(albedo / Color::PBRLightingScale); float3 multiBounceSSGIAo = MultiBounceAO(linAlbedo, ssgiAo); diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 6af6545433..e611a018cc 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -344,9 +344,7 @@ struct PS_OUTPUT float4 Specular: SV_Target4; float4 Reflectance: SV_Target5; float4 Masks: SV_Target6; -# if defined(SNOW) - float4 Parameters: SV_Target7; -# endif + float4 Masks2: SV_Target7; }; #else struct PS_OUTPUT @@ -2184,9 +2182,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) rawBaseColor = Triplanar::SampleStochasticBias(TexColorSampler, SampColorSampler, projWorldPos, triWeights, ProjectedUVParams2.y, SharedData::MipBias, screenNoise); baseColor = float4(Color::Diffuse(rawBaseColor.rgb), rawBaseColor.a); worldNormal.xyz = projectedNormal; -# if defined(SNOW) - psout.Parameters.y = 1; -# endif // SNOW # elif !defined(FACEGEN) && !defined(MULTI_LAYER_PARALLAX) && !defined(PARALLAX) && !defined(SPARKLE) if (ProjectedUVParams3.w > 0.5) { float diffuseNormalScale = ProjectedUVParams3.x * ProjectedUVParams.z; @@ -2215,18 +2210,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SNOW) useSnowDecalSpecular = true; - psout.Parameters.y = GetSnowParameterY(projectedMaterialWeight, baseColor.w); # endif // SNOW } else { if (projWeight > 0) { baseColor.xyz = Color::Diffuse(ProjectedUVParams2.xyz); # if defined(SNOW) useSnowDecalSpecular = true; - psout.Parameters.y = GetSnowParameterY(projWeight, baseColor.w); -# endif // SNOW - } else { -# if defined(SNOW) - psout.Parameters.y = 0; # endif // SNOW } } @@ -2236,12 +2225,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPECULAR # endif // SPARKLE -# elif defined(SNOW) -# if defined(LANDSCAPE) - psout.Parameters.y = landSnowMask; -# else - psout.Parameters.y = baseColor.w; -# endif // LANDSCAPE # endif // SNOW # if defined(WORLD_MAP) @@ -3077,13 +3060,14 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(HAIR) float3 vertexColor = lerp(1, Color::ColorToLinear(TintColor.xyz), Color::ColorToLinear(input.Color.y)); + float vertexAO = 1; # if defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) vertexColor = 1; # endif # elif defined(SKYLIGHTING) float3 vertexColor = input.Color.xyz; - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + float vertexAO = Color::ColorToLinear(max(max(vertexColor.r, vertexColor.g), vertexColor.b).xxx).x; # if defined(TRUE_PBR) vertexAO = lerp(1, vertexAO, SharedData::truePBRSettings.VertexAOStrength); vertexColor = 1; @@ -3096,6 +3080,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 vertexColor = input.Color.xyz; # endif + float vertexAO = Color::ColorToLinear(max(max(vertexColor.r, vertexColor.g), vertexColor.b).xxx).x; # endif // defined (HAIR) # if defined(IBL) @@ -3493,16 +3478,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) PomOffsetTex[uint2(input.Position.xy)] = hasPOM ? pixelOffset : Stereo::POM_NO_DATA; # endif -# if defined(SNOW) -# if defined(TRUE_PBR) - psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); - psout.Parameters.y = 0; -# else - psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); -# endif - psout.Parameters.w = psout.Diffuse.w; -# endif - float masksZ = Color::RGBToYCoCg(directionalAmbientColor).x; # if defined(SSS) && defined(SKIN) @@ -3511,6 +3486,10 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Masks = float4(0, 0, masksZ, psout.Diffuse.w); # endif + // Stored as 1 - vertexAO so the cleared default (0) means no occlusion + // for pixels that do not write to this RT (sky, water, grass, effects). + psout.Masks2 = float4(1.0 - vertexAO, 0, 0, 0); + float stochasticBlend = (screenNoise * screenNoise) < psout.Diffuse.w ? 1.0 : 0.0; psout.NormalGlossiness.w = stochasticBlend; # endif diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index e7ef06f9f8..8aaeecb4db 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -358,9 +358,7 @@ struct PS_OUTPUT float4 Reflectance: SV_Target5; # endif // TRUE_PBR float4 Masks: SV_Target6; -# if defined(TRUE_PBR) - float4 Parameters: SV_Target7; -# endif // TRUE_PBR + float4 Masks2: SV_Target7; # endif // RENDER_DEPTH }; #else @@ -374,6 +372,7 @@ struct PS_OUTPUT float4 Normal: SV_Target2; float4 Albedo: SV_Target3; float4 Masks: SV_Target6; + float4 Masks2: SV_Target7; # endif }; #endif @@ -633,7 +632,8 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lightsDiffuseColor += dirLightColor * dirDetailedShadow * saturate(dirLightAngle) * Color::VanillaNormalization(); float3 vertexColor = Color::ColorToLinear(input.Color.xyz); - vertexColor /= max(max(max(vertexColor.r, vertexColor.g), vertexColor.b), EPSILON_DIVISION); + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) # if defined(VR) @@ -641,7 +641,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -787,7 +786,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Albedo = float4(Color::IrradianceToGamma(indirectDiffuseLobeWeight), 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), 1 - pbrSurfaceProperties.Roughness, 1); psout.Reflectance = float4(indirectSpecularLobeWeight, 1); - psout.Parameters = float4(0, 0, 1, 1); # else psout.Albedo = float4(albedo, 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), specColor.w, 1); @@ -795,6 +793,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Specular = float4(specularColor, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); + psout.Masks2 = float4(1.0 - vertexAO, 0, 0, 0); # endif return psout; } @@ -900,7 +899,8 @@ PS_OUTPUT main(PS_INPUT input) float3 normal = -normalize(cross(ddx, ddy)); float3 vertexColor = Color::ColorToLinear(input.Color.xyz); - vertexColor /= max(max(max(vertexColor.r, vertexColor.g), vertexColor.b), EPSILON_DIVISION); + float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); + vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) # if defined(VR) @@ -908,7 +908,6 @@ PS_OUTPUT main(PS_INPUT input) # else float3 positionMSSkylight = input.WorldPosition.xyz; # endif - float vertexAO = max(max(vertexColor.r, vertexColor.g), vertexColor.b); float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -950,6 +949,7 @@ PS_OUTPUT main(PS_INPUT input) psout.Albedo = float4(albedo, 1); psout.Masks = float4(0, 0, Color::RGBToYCoCg(directionalAmbientColor).x, 0); + psout.Masks2 = float4(1.0 - vertexAO, 0, 0, 0); # endif return psout; diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 62146505f6..9c9a747960 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -109,6 +109,8 @@ void Deferred::SetupResources() SetupRenderTarget(NORMALROUGHNESS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R10G10B10A2_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Masks SetupRenderTarget(MASKS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R11G11B10_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); + // Masks2 (vertexAO; fp16 to allow blending) + SetupRenderTarget(MASKS2, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // TAA Water Buffers SetupRenderTarget(RE::RENDER_TARGETS::kWATER_1, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); @@ -243,7 +245,7 @@ void Deferred::StartDeferred() SPECULAR, REFLECTANCE, MASKS, - RE::RENDER_TARGET::kNONE + MASKS2 }; for (uint i = 2; i < 8; i++) { @@ -317,6 +319,7 @@ void Deferred::DeferredPasses() auto albedo = renderer->GetRuntimeData().renderTargets[ALBEDO]; auto normalRoughness = renderer->GetRuntimeData().renderTargets[NORMALROUGHNESS]; auto masks = renderer->GetRuntimeData().renderTargets[MASKS]; + auto masks2 = renderer->GetRuntimeData().renderTargets[MASKS2]; auto main = renderer->GetRuntimeData().renderTargets[forwardRenderTargets[0]]; auto normals = renderer->GetRuntimeData().renderTargets[forwardRenderTargets[2]]; @@ -361,7 +364,7 @@ void Deferred::DeferredPasses() dynamicCubemaps.loaded ? dynamicCubemaps.envTexture->srv.get() : nullptr, // t6 EnvTexture dynamicCubemaps.loaded ? dynamicCubemaps.envReflectionsTexture->srv.get() : nullptr, // t7 EnvReflectionsTexture dynamicCubemaps.loaded && skylighting.loaded ? skylighting.texProbeArray->srv.get() : nullptr, // t8 SkylightingProbeArray - nullptr, // t9 unused + masks2.SRV, // t9 Masks2Texture (vertexAO in .x) ssgi_ao, // t10 SsgiAoTexture ssgi_hq_spec ? nullptr : ssgi_y, // t11 SsgiYTexture ssgi_hq_spec ? nullptr : ssgi_cocg, // t12 SsgiCoCgTexture From d0550a54af04060f7c3a93d0fe5dbacb092fe7be Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:06:53 +0100 Subject: [PATCH 37/55] feat(sss): add diffuse extraction pre-pass and scatter modes (#2407) --- .../Shaders/SubsurfaceScattering/Burley.hlsli | 11 ++- .../DiffuseExtractionCS.hlsl | 18 ++++ .../SubsurfaceScattering/SSSCommon.hlsli | 41 +++++++++ .../SubsurfaceScattering/SeparableSSS.hlsli | 50 +++++----- .../SubsurfaceScattering/SeparableSSSCS.hlsl | 29 +++--- .../CommunityShaders/Translations/en.json | 9 +- .../CommunityShaders/Translations/zh_CN.json | 9 +- src/Features/SubsurfaceScattering.cpp | 91 ++++++++++++++++--- src/Features/SubsurfaceScattering.h | 20 +++- 9 files changed, 210 insertions(+), 68 deletions(-) create mode 100644 features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl create mode 100644 features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index ea4a94f245..6fb969ff35 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -58,7 +58,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA } float4 surfaceAlbedo = AlbedoTexture[DTid]; - float3 originalColor = Color::IrradianceToLinear(centerColor.xyz / max(surfaceAlbedo.xyz, EPSILON_SSS_ALBEDO)); + float3 originalColor = centerColor.xyz; float4 diffuseMeanFreePath = humanProfile ? MeanFreePathHuman : MeanFreePathBase; diffuseMeanFreePath.xyz = float3(max(diffuseMeanFreePath.x, 1e-5f), max(diffuseMeanFreePath.y, 1e-5f), max(diffuseMeanFreePath.z, 1e-5f)); @@ -112,7 +112,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA if (!mask) continue; - float3 sampleColor = Color::IrradianceToLinear(ColorTexture[samplePixcoord].xyz * maskSample / max(AlbedoTexture[samplePixcoord].xyz, EPSILON_SSS_ALBEDO)); + float3 sampleColor = ColorTexture[samplePixcoord].xyz * maskSample; float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x); float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy); float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz); @@ -130,8 +130,11 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA colorSum *= any(weightSum == 0.0f) ? 0.0f : (1.0f / weightSum); colorSum = lerp(colorSum, originalColor, saturate(centerWeight)); - float3 color = Color::IrradianceToGamma(colorSum) * AlbedoTexture[DTid.xy].xyz; - color = lerp(centerColor.xyz, color, saturate(sssAmount)); + + float3 albedo = AlbedoTexture[DTid.xy].xyz; + float3 color = SSSApplyAlbedo(Color::IrradianceToGamma(colorSum), albedo, SSS_SCATTER_MODE_POST); + float3 centerColorRestored = SSSApplyAlbedo(Color::IrradianceToGamma(originalColor), albedo, SSS_SCATTER_MODE_POST); + color = lerp(centerColorRestored, color, saturate(sssAmount > 0.0)); float4 outColor = float4(color, ColorTexture[DTid.xy].w); return outColor; diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl new file mode 100644 index 0000000000..c4176fd893 --- /dev/null +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/DiffuseExtractionCS.hlsl @@ -0,0 +1,18 @@ +RWTexture2D OutputRW : register(u0); + +Texture2D ColorTexture : register(t0); +Texture2D AlbedoTexture : register(t3); + +#include "Common/Color.hlsli" +#include "Common/SharedData.hlsli" +#include "SubsurfaceScattering/SSSCommon.hlsli" + +[numthreads(8, 8, 1)] void main(uint3 DTid : SV_DispatchThreadID) { + if (any(DTid.xy >= uint2(SharedData::BufferDim.xy))) + return; + + float4 color = ColorTexture[DTid.xy]; + color.rgb = SSSRemoveAlbedo(color.rgb, AlbedoTexture[DTid.xy].rgb, ScatterMode); + color.rgb = Color::IrradianceToLinear(color.rgb); + OutputRW[DTid.xy] = color; +} diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli new file mode 100644 index 0000000000..7441f34083 --- /dev/null +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SSSCommon.hlsli @@ -0,0 +1,41 @@ +#ifndef SSS_COMMON_HLSLI +#define SSS_COMMON_HLSLI + +#include "Common/Math.hlsli" + +#define SSSS_N_SAMPLES 21 + +#define SSS_SCATTER_MODE_PRE 0 +#define SSS_SCATTER_MODE_POST 1 +#define SSS_SCATTER_MODE_PRE_POST 2 + +cbuffer PerFrameSSS : register(b1) +{ + float4 Kernels[SSSS_N_SAMPLES + SSSS_N_SAMPLES]; + float4 BaseProfile; + float4 HumanProfile; + float SSSS_FOVY; + uint BurleySamples; + uint ScatterMode; + uint pad; + float4 MeanFreePathBase; + float4 MeanFreePathHuman; +}; + +float3 SSSRemoveAlbedo(float3 color, float3 albedo, uint mode) +{ + if (mode == SSS_SCATTER_MODE_PRE) + return color; + float3 divisor = (mode == SSS_SCATTER_MODE_PRE_POST) ? sqrt(albedo) : albedo; + return color / max(divisor, EPSILON_SSS_ALBEDO); +} + +float3 SSSApplyAlbedo(float3 irradiance, float3 albedo, uint mode) +{ + if (mode == SSS_SCATTER_MODE_PRE) + return irradiance; + float3 multiplier = (mode == SSS_SCATTER_MODE_PRE_POST) ? sqrt(albedo) : albedo; + return irradiance * multiplier; +} + +#endif diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli index 65b89e7def..18296789b2 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSS.hlsli @@ -90,26 +90,27 @@ //----------------------------------------------------------------------------- #include "Common/Math.hlsli" +#include "Common/Random.hlsli" float4 SSSSBlurCS( - uint2 DTid, float2 texcoord, float2 dir, float sssAmount, bool humanProfile) { - // Fetch color of current pixel: - float4 colorM = ColorTexture[DTid.xy]; + // texcoord is in DR-space UVs (coming from DTid / full buffer dim), so it already + // addresses the rendered sub-rectangle of the texture. Non-DR UVs are derived for + // eye-half detection during clamping. + float2 texcoordNonDR = texcoord * FrameBuffer::DynamicResolutionParams2.xy; -#if defined(HORIZONTAL) - colorM.rgb = Color::IrradianceToLinear(colorM.rgb); -#endif + // Input is already linear and albedo-free from the pre-pass + float4 colorM = ColorTexture.SampleLevel(PointSampler, texcoord, 0); if (sssAmount == 0) return colorM; // Fetch linear depth of current pixel: - float depthM = DepthTexture[DTid.xy].r; + float depthM = DepthTexture.SampleLevel(PointSampler, texcoord, 0).r; depthM = SharedData::GetScreenDepth(depthM); float2 profile = humanProfile ? HumanProfile.xy : BaseProfile.xy; @@ -124,38 +125,31 @@ float4 SSSSBlurCS( float scale = distanceToProjectionWindow / depthM; // Calculate the final step to fetch the surrounding pixels: - float2 finalStep = scale * SharedData::BufferDim.xy * dir; + float2 finalStep = scale * dir; finalStep *= sssAmount; finalStep *= profile.x; // Modulate it using the profile - finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. - -#if defined(VR) - finalStep.x *= 0.5; // Halve horizontal screen resolution - uint eyeIndex = texcoord.x >= 0.5; // 0 = left 1 = right - uint bufferDimHalfX = uint(SharedData::BufferDim.x * 0.5); - uint2 minCoord = uint2(eyeIndex ? bufferDimHalfX : 0, 0); - uint2 maxCoord = uint2(eyeIndex ? SharedData::BufferDim.x : bufferDimHalfX, SharedData::BufferDim.y); -#else - uint2 minCoord = uint2(0, 0); - uint2 maxCoord = uint2(SharedData::BufferDim.x, SharedData::BufferDim.y); -#endif + // Scale the step into DR-UV space so blur width in rendered pixels stays consistent. + finalStep *= FrameBuffer::DynamicResolutionParams1.xy; + + // Per-pixel rotation to break separable axis-aligned banding + float jitter = Random::InterleavedGradientNoise(texcoord * SharedData::BufferDim.xy, SharedData::FrameCount) * Math::TAU; + float2x2 rotationMatrix = float2x2(cos(jitter), sin(jitter), -sin(jitter), cos(jitter)); // Accumulate the other samples: for (uint i = kernelOffset + 1; i < kernelOffset + SSSS_N_SAMPLES; i++) { float2 offset = Kernels[i].a * finalStep; - uint2 coords = DTid.xy + int2(offset + 0.5); + // Apply randomized rotation + offset = mul(offset, rotationMatrix); - // Clamp for dynamic resolution - coords = clamp(coords, minCoord, maxCoord); + float2 sampleCoord = texcoord + offset; - float3 color = ColorTexture[coords].rgb; + // Clamp to the DR-rendered region (per-eye in VR) to avoid sampling outside it. + sampleCoord = FrameBuffer::ClampDynamicResolutionAdjustedScreenPosition(sampleCoord, texcoordNonDR); -#if defined(HORIZONTAL) - color.rgb = Color::IrradianceToLinear(color.rgb); -#endif + float3 color = ColorTexture.SampleLevel(PointSampler, sampleCoord, 0).rgb; - float depth = DepthTexture[coords].r; + float depth = DepthTexture.SampleLevel(PointSampler, sampleCoord, 0).r; depth = SharedData::GetScreenDepth(depth); // If the difference in depth is huge, we lerp color back to "colorM": diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl index f4a955f504..e6457a05af 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl @@ -6,23 +6,12 @@ Texture2D MaskTexture : register(t2); Texture2D AlbedoTexture : register(t3); Texture2D NormalTexture : register(t4); -#define SSSS_N_SAMPLES 21 - -cbuffer PerFrameSSS : register(b1) -{ - float4 Kernels[SSSS_N_SAMPLES + SSSS_N_SAMPLES]; - float4 BaseProfile; - float4 HumanProfile; - float SSSS_FOVY; - uint BurleySamples; - uint2 pad; - float4 MeanFreePathBase; - float4 MeanFreePathHuman; -}; +SamplerState PointSampler : register(s0); #include "Common/Color.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" +#include "SubsurfaceScattering/SSSCommon.hlsli" #if defined(BURLEY) # include "SubsurfaceScattering/Burley.hlsli" @@ -41,17 +30,20 @@ cbuffer PerFrameSSS : register(b1) #if defined(BURLEY) float sssAmount = MaskTexture[DTid.xy].x; - bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); - SSSRW[DTid.xy] = max(0, color); + if (sssAmount > 0.0) { + bool humanProfile = MaskTexture[DTid.xy].y > 0.0; + + float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); + SSSRW[DTid.xy] = max(0, color); + } #elif defined(HORIZONTAL) float sssAmount = MaskTexture[DTid.xy].x; bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = SSSSBlurCS(DTid.xy, texCoord, float2(1.0, 0.0), sssAmount, humanProfile); + float4 color = SSSSBlurCS(texCoord, float2(1.0, 0.0), sssAmount, humanProfile); SSSRW[DTid.xy] = max(0, color); #else @@ -61,8 +53,9 @@ cbuffer PerFrameSSS : register(b1) if (sssAmount > 0.0) { bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = SSSSBlurCS(DTid.xy, texCoord, float2(0.0, 1.0), sssAmount, humanProfile); + float4 color = SSSSBlurCS(texCoord, float2(0.0, 1.0), sssAmount, humanProfile); color.rgb = Color::IrradianceToGamma(color.rgb); + color.rgb = SSSApplyAlbedo(color.rgb, AlbedoTexture[DTid.xy].rgb, ScatterMode); SSSRW[DTid.xy] = float4(color.rgb, 1.0); } diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index ddfafe0519..4d6b3de2dd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -1202,19 +1202,26 @@ "feature.skylighting.rebuild": "Rebuild Skylighting", "feature.skylighting.rebuild_tooltip": "Changes below require rebuilding, a loading screen, or moving away from the current location to apply.", "feature.skylighting.specular_min_visibility": "Specular Min Visibility", + "feature.sss.albedo_handling": "Albedo Handling", "feature.sss.base_profile": "Base Profile", "feature.sss.blur_radius": "Blur Radius", "feature.sss.blur_radius_tooltip": "Blur radius.", "feature.sss.burley": "Burley", "feature.sss.burley_samples": "Burley Samples", "feature.sss.enable_character_lighting": "Enable Character Lighting", - "feature.sss.enable_character_lighting_tooltip": "Vanilla feature, not recommended.", + "feature.sss.enable_character_lighting_tooltip": "Vanilla feature.", "feature.sss.falloff": "Falloff", "feature.sss.human_profile": "Human Profile", "feature.sss.mean_free_path_color": "Mean Free Path Color", "feature.sss.mean_free_path_color_tooltip": "Controls how far light goes into the subsurface in the red, green, and blue channel. It is scaled by the Mean Free Path Distance.", "feature.sss.mean_free_path_distance": "Mean Free Path Distance", "feature.sss.mean_free_path_distance_tooltip": "Controls the distance that Mean Free Path Color goes into subsurface.", + "feature.sss.post_scatter": "Post-scatter", + "feature.sss.post_scatter_tooltip": "Divide out albedo, blur the irradiance, multiply albedo back. Preserves texture detail.", + "feature.sss.pre_and_post_scatter": "Pre and Post", + "feature.sss.pre_and_post_scatter_tooltip": "Split albedo across the blur using sqrt(albedo) on each side. A physically motivated middle ground.", + "feature.sss.pre_scatter": "Pre-scatter", + "feature.sss.pre_scatter_tooltip": "Blur the lit color directly. Fastest, but blurs albedo texture detail along with lighting.", "feature.sss.separable_sss": "Separable SSS", "feature.sss.settings": "Settings", "feature.sss.strength": "Strength", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index c80ce9dabd..30264ab701 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -1201,19 +1201,26 @@ "feature.skylighting.rebuild": "重建天光", "feature.skylighting.rebuild_tooltip": "以下更改需要重建、加载屏幕或离开当前位置才能应用。", "feature.skylighting.specular_min_visibility": "镜面反射最小可见度", + "feature.sss.albedo_handling": "反照率处理", "feature.sss.base_profile": "基础预设", "feature.sss.blur_radius": "模糊半径", "feature.sss.blur_radius_tooltip": "模糊半径。", "feature.sss.burley": "Burley", "feature.sss.burley_samples": "Burley采样数", "feature.sss.enable_character_lighting": "启用角色光照", - "feature.sss.enable_character_lighting_tooltip": "原版功能,不推荐。", + "feature.sss.enable_character_lighting_tooltip": "原版功能。", "feature.sss.falloff": "衰减", "feature.sss.human_profile": "人类预设", "feature.sss.mean_free_path_color": "平均自由路径颜色", "feature.sss.mean_free_path_color_tooltip": "控制光在红色、绿色和蓝色通道中进入次表面的距离。由平均自由路径距离缩放。", "feature.sss.mean_free_path_distance": "平均自由路径距离", "feature.sss.mean_free_path_distance_tooltip": "控制平均自由路径颜色进入次表面的距离。", + "feature.sss.post_scatter": "后散射", + "feature.sss.post_scatter_tooltip": "先移除反照率,模糊辐照度,再乘回反照率。保留纹理细节。", + "feature.sss.pre_and_post_scatter": "前后散射", + "feature.sss.pre_and_post_scatter_tooltip": "使用 sqrt(albedo) 在模糊两侧分配反照率。基于物理的折中方案。", + "feature.sss.pre_scatter": "前散射", + "feature.sss.pre_scatter_tooltip": "直接模糊光照颜色。最快,但会模糊反照率纹理细节。", "feature.sss.separable_sss": "可分离SSS", "feature.sss.settings": "设置", "feature.sss.strength": "强度", diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index 384a7c7c61..591d1ae194 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -15,6 +15,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( EnableCharacterLighting, CharacterLightingStrength, SSMode, + ScatterMode, BaseProfile, HumanProfile, BurleySamples, @@ -26,7 +27,7 @@ void SubsurfaceScattering::DrawSettings() if (ImGui::TreeNodeEx(T(TKEY("settings"), "Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Checkbox(T(TKEY("enable_character_lighting"), "Enable Character Lighting"), (bool*)&settings.EnableCharacterLighting); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", T(TKEY("enable_character_lighting_tooltip"), "Vanilla feature, not recommended.")); + ImGui::Text("%s", T(TKEY("enable_character_lighting_tooltip"), "Vanilla feature.")); } if (settings.EnableCharacterLighting) { ImGui::SliderFloat(T(TKEY("strength"), "Strength"), &settings.CharacterLightingStrength, 0, 5, "%.2f"); @@ -36,6 +37,25 @@ void SubsurfaceScattering::DrawSettings() ImGui::SameLine(); ImGui::RadioButton(T(TKEY("burley"), "Burley"), &settings.SSMode, 1); + if (settings.SSMode == 0) { + ImGui::Spacing(); + ImGui::Text("%s", T(TKEY("albedo_handling"), "Albedo Handling")); + ImGui::RadioButton(T(TKEY("pre_scatter"), "Pre-scatter"), &settings.ScatterMode, kPreScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("pre_scatter_tooltip"), "Blur the lit color directly. Fastest, but blurs albedo texture detail along with lighting.")); + } + ImGui::SameLine(); + ImGui::RadioButton(T(TKEY("post_scatter"), "Post-scatter"), &settings.ScatterMode, kPostScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("post_scatter_tooltip"), "Divide out albedo, blur the irradiance, multiply albedo back. Preserves texture detail.")); + } + ImGui::SameLine(); + ImGui::RadioButton(T(TKEY("pre_and_post_scatter"), "Pre and Post"), &settings.ScatterMode, kPreAndPostScatter); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("pre_and_post_scatter_tooltip"), "Split albedo across the blur using sqrt(albedo) on each side. A physically motivated middle ground.")); + } + } + if (settings.SSMode == 0) { if (ImGui::TreeNodeEx(T(TKEY("base_profile"), "Base Profile"), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat(T(TKEY("blur_radius"), "Blur Radius"), &settings.BaseProfile.BlurRadius, 0, 3, "%.2f"); @@ -218,6 +238,10 @@ void SubsurfaceScattering::DrawSSS() blurCBData.HumanProfile = { settings.HumanProfile.BlurRadius, settings.HumanProfile.Thickness, 0, 0 }; blurCBData.BurleySamples = settings.BurleySamples; + // Burley always does full albedo removal/reapply; scatter mode only applies to Separable SSS. + blurCBData.ScatterMode = (settings.SSMode == 0) + ? (uint)std::clamp(settings.ScatterMode, (int)kPreScatter, (int)kPreAndPostScatter) + : (uint)kPostScatter; blurCBData.MeanFreePathBase = settings.MeanFreePathBase; blurCBData.MeanFreePathHuman = settings.MeanFreePathHuman; @@ -231,6 +255,7 @@ void SubsurfaceScattering::DrawSSS() { ID3D11Buffer* buffer[1] = { blurCB->CB() }; context->CSSetConstantBuffers(1, 1, buffer); + context->CSSetSamplers(0, 1, &globals::deferred->pointSampler); auto main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; @@ -238,9 +263,6 @@ void SubsurfaceScattering::DrawSSS() auto albedo = renderer->GetRuntimeData().renderTargets[ALBEDO]; auto normal = renderer->GetRuntimeData().renderTargets[NORMALROUGHNESS]; - ID3D11UnorderedAccessView* uav = blurHorizontalTemp->uav.get(); - context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); - ID3D11ShaderResourceView* views[5]; views[0] = main.SRV; views[1] = Util::GetCurrentSceneDepthSRV(true); @@ -250,23 +272,46 @@ void SubsurfaceScattering::DrawSSS() context->CSSetShaderResources(0, ARRAYSIZE(views), views); + // Pre-pass: remove albedo from diffuse, write to diffuseNoAlbedoTex + { + TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Prepass"); + + ID3D11UnorderedAccessView* uav = diffuseNoAlbedoTex->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + + auto shader = GetComputeShaderPrepass(); + context->CSSetShader(shader, nullptr, 0); + + context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } + + // Swap color input to pre-processed texture + views[0] = diffuseNoAlbedoTex->srv.get(); + context->CSSetShaderResources(0, 1, views); + if (settings.SSMode == 0) { - // Horizontal pass to temporary texture + // Horizontal pass: diffuseNoAlbedoTex -> blurHorizontalTemp { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Horizontal"); + ID3D11UnorderedAccessView* uav = blurHorizontalTemp->uav.get(); + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + auto shader = GetComputeShaderHorizontalBlur(); context->CSSetShader(shader, nullptr, 0); globals::profiler->BeginPass("SubsurfaceScattering::HorizontalBlur"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); globals::profiler->EndPass(); - } - uav = nullptr; - context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + uav = nullptr; + context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + } - // Vertical pass to main texture + // Vertical pass: blurHorizontalTemp -> main { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Vertical"); @@ -284,18 +329,19 @@ void SubsurfaceScattering::DrawSSS() globals::profiler->EndPass(); } } else if (settings.SSMode == 1) { - // Burley pass to main texture + // Burley pass: diffuseNoAlbedoTex -> main (SSS pixels only) { TracyD3D11Zone(globals::state->tracyCtx, "Subsurface Scattering - Burley"); + ID3D11UnorderedAccessView* uavs[1] = { main.UAV }; + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + auto shader = GetComputeShaderBurley(); context->CSSetShader(shader, nullptr, 0); globals::profiler->BeginPass("SubsurfaceScattering::Burley"); context->Dispatch(dispatchCount.x, dispatchCount.y, 1); globals::profiler->EndPass(); - - context->CopyResource(main.texture, blurHorizontalTemp->resource.get()); } } } @@ -303,6 +349,9 @@ void SubsurfaceScattering::DrawSSS() ID3D11Buffer* buffer = nullptr; context->CSSetConstantBuffers(1, 1, &buffer); + ID3D11SamplerState* nullSampler = nullptr; + context->CSSetSamplers(0, 1, &nullSampler); + ID3D11ShaderResourceView* views[5]{ nullptr, nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); @@ -338,6 +387,10 @@ void SubsurfaceScattering::SetupResources() blurHorizontalTemp = new Texture2D(texDesc); blurHorizontalTemp->CreateSRV(srvDesc); blurHorizontalTemp->CreateUAV(uavDesc); + + diffuseNoAlbedoTex = new Texture2D(texDesc); + diffuseNoAlbedoTex->CreateSRV(srvDesc); + diffuseNoAlbedoTex->CreateUAV(uavDesc); } } @@ -368,6 +421,7 @@ void SubsurfaceScattering::RestoreDefaultSettings() void SubsurfaceScattering::LoadSettings(json& o_json) { settings = o_json; + settings.ScatterMode = std::clamp(settings.ScatterMode, (int)kPreScatter, (int)kPreAndPostScatter); } void SubsurfaceScattering::SaveSettings(json& o_json) @@ -377,6 +431,10 @@ void SubsurfaceScattering::SaveSettings(json& o_json) void SubsurfaceScattering::ClearShaderCache() { + if (prepassSS) { + prepassSS->Release(); + prepassSS = nullptr; + } if (horizontalSSBlur) { horizontalSSBlur->Release(); horizontalSSBlur = nullptr; @@ -391,6 +449,15 @@ void SubsurfaceScattering::ClearShaderCache() } } +ID3D11ComputeShader* SubsurfaceScattering::GetComputeShaderPrepass() +{ + if (!prepassSS) { + logger::debug("Compiling prepassSS"); + prepassSS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\SubsurfaceScattering\\DiffuseExtractionCS.hlsl", {}, "cs_5_0"); + } + return prepassSS; +} + ID3D11ComputeShader* SubsurfaceScattering::GetComputeShaderHorizontalBlur() { if (!horizontalSSBlur) { diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index 3d8a32e7c0..7ebd3ae33e 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -15,13 +15,21 @@ struct SubsurfaceScattering : Feature float3 Falloff; }; + enum ScatterMode : int + { + kPreScatter = 0, + kPostScatter = 1, + kPreAndPostScatter = 2, + }; + struct Settings { uint EnableCharacterLighting = false; float CharacterLightingStrength = 1.0f; - int SSMode = 1; - DiffusionProfile BaseProfile{ 0.5f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 0.56f, 0.56f, 0.56f } }; - DiffusionProfile HumanProfile{ 0.5f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 1.0f, 0.37f, 0.3f } }; + int SSMode = 0; + int ScatterMode = kPreAndPostScatter; + DiffusionProfile BaseProfile{ 1.0f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 0.56f, 0.56f, 0.56f } }; + DiffusionProfile HumanProfile{ 1.0f, 1.0f, { 0.48f, 0.41f, 0.28f }, { 1.0f, 0.37f, 0.3f } }; uint BurleySamples = 16; float4 MeanFreePathBase = { 0.56f, 0.56f, 0.56f, 2.67f }; float4 MeanFreePathHuman = { 1.0f, 0.37f, 0.3f, 2.67f }; @@ -45,7 +53,8 @@ struct SubsurfaceScattering : Feature float4 HumanProfile; float SSSS_FOVY; uint BurleySamples; - uint pad[2]; + uint ScatterMode; + uint pad; float4 MeanFreePathBase; float4 MeanFreePathHuman; }; @@ -59,7 +68,9 @@ struct SubsurfaceScattering : Feature bool validMaterials = false; Texture2D* blurHorizontalTemp = nullptr; + Texture2D* diffuseNoAlbedoTex = nullptr; + ID3D11ComputeShader* prepassSS = nullptr; ID3D11ComputeShader* horizontalSSBlur = nullptr; ID3D11ComputeShader* verticalSSBlur = nullptr; ID3D11ComputeShader* burleySS = nullptr; @@ -99,6 +110,7 @@ struct SubsurfaceScattering : Feature virtual void SaveSettings(json& o_json) override; virtual void ClearShaderCache() override; + ID3D11ComputeShader* GetComputeShaderPrepass(); ID3D11ComputeShader* GetComputeShaderHorizontalBlur(); ID3D11ComputeShader* GetComputeShaderVerticalBlur(); ID3D11ComputeShader* GetComputeShaderBurley(); From bcea965e5e65eae85ea966491f1c0e27e311d6ca Mon Sep 17 00:00:00 2001 From: davo0411 Date: Sun, 7 Jun 2026 18:07:17 +1000 Subject: [PATCH 38/55] fix: unified water bad cell guards (#2482) --- src/Features/UnifiedWater/WaterCache.cpp | 75 ++++++++++++++++++++---- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/src/Features/UnifiedWater/WaterCache.cpp b/src/Features/UnifiedWater/WaterCache.cpp index 38850b5973..31507f2a44 100644 --- a/src/Features/UnifiedWater/WaterCache.cpp +++ b/src/Features/UnifiedWater/WaterCache.cpp @@ -18,6 +18,18 @@ namespace height != FLT_MAX && std::fabs(height) < kMaxValidCellHeight; } + + RE::TESWaterForm* LookupWaterForm(const RE::FormID formID) + { + if (!formID) + return nullptr; + + auto* form = RE::TESWaterForm::LookupByID(formID); + if (!form || form->formType != RE::FormType::Water) + return nullptr; + + return form; + } } bool WaterCache::SetCurrentWorldSpace(const RE::TESWorldSpace* worldSpace) @@ -168,6 +180,7 @@ bool WaterCache::LoadCaches() auto worldSpaces = GetValidWorldSpaces(); auto newCacheMap = std::make_shared(); + uint32_t unavailableCount = 0; for (auto& worldSpace : worldSpaces) { const auto editorID = worldSpace ? worldSpace->GetFormEditorID() : nullptr; @@ -185,7 +198,8 @@ bool WaterCache::LoadCaches() const auto fileName = std::format("{}_cache.wc", key); if (!TryReadCacheFromFile(fileName, diskCache.header, diskCache.instructions)) { logger::info("[Unified Water] [Cache] Could not locate disk cache for {}", key); - return false; + unavailableCount++; + continue; } logger::debug("[Unified Water] [Cache] Loaded cache for {} - Bounds {},{} {},{} - Instructions {}", editorID, diskCache.header.bounds.minX, diskCache.header.bounds.minY, diskCache.header.bounds.maxX, diskCache.header.bounds.maxY, diskCache.header.dataCount); @@ -193,12 +207,21 @@ bool WaterCache::LoadCaches() auto newCache = std::make_unique(); if (!TryBuildRuntimeCache(diskCache, *newCache)) { logger::warn("[Unified Water] [Cache] Failed to build runtime cache for {}", key); - return false; + unavailableCount++; + continue; } newCacheMap->emplace(std::move(key), std::move(newCache)); } + if (unavailableCount) { + logger::info("[Unified Water] [Cache] Loaded {} / {} worldspace caches ({} unavailable)", newCacheMap->size(), worldSpaces.size(), unavailableCount); + } + + if (newCacheMap->empty()) { + return false; + } + std::atomic_store_explicit(&cacheMap, std::const_pointer_cast(newCacheMap), std::memory_order_release); if (!currentWorldSpace.empty()) { @@ -371,6 +394,10 @@ bool WaterCache::BuildDiskCache(RE::TESWorldSpace* worldSpace, DiskCache& diskCa int32_t precacheFallbackCount = 0; int32_t skippedMissingCellDataCount = 0; int32_t skippedInvalidHeightCount = 0; + int32_t skippedUnresolvedFormCount = 0; + int32_t firstUnresolvedFormX = 0; + int32_t firstUnresolvedFormY = 0; + RE::FormID firstUnresolvedFormID = 0; for (auto y = minY; y <= maxY; ++y) { for (auto x = minX; x <= maxX; ++x) { @@ -413,10 +440,23 @@ bool WaterCache::BuildDiskCache(RE::TESWorldSpace* worldSpace, DiskCache& diskCa formID = 0; } - RE::TESWaterForm* form = formID ? RE::TESWaterForm::LookupByID(formID) : nullptr; - if ((formID && !form) || (form && form->formType != RE::FormType::Water)) { - logger::warn("[Unified Water] [Cache] {}: Failed to load WaterForm {:08X}", editorID.c_str(), formID); - return false; + const RE::FormID requestedFormID = formID; + RE::TESWaterForm* form = LookupWaterForm(formID); + + if (!form && requestedFormID && worldSpace->worldWater && requestedFormID != worldSpace->worldWater->formID) { + formID = worldSpace->worldWater->formID; + form = LookupWaterForm(formID); + } + + if (requestedFormID && !form) { + if (!skippedUnresolvedFormCount) { + firstUnresolvedFormX = x; + firstUnresolvedFormY = y; + firstUnresolvedFormID = requestedFormID; + } + skippedUnresolvedFormCount++; + cellData[idx] = {}; + continue; } if (form) @@ -426,6 +466,11 @@ bool WaterCache::BuildDiskCache(RE::TESWorldSpace* worldSpace, DiskCache& diskCa } } + if (skippedUnresolvedFormCount) { + logger::warn("[Unified Water] [Cache] {}: Skipped {} cells due to unresolvable water forms (first at {},{} form {:08X})", + editorID.c_str(), skippedUnresolvedFormCount, firstUnresolvedFormX, firstUnresolvedFormY, firstUnresolvedFormID); + } + if (precacheFallbackCount || skippedMissingCellDataCount || skippedInvalidHeightCount) { logger::debug("[Unified Water] [Cache] {}: {} cells used precache fallback, {} cells skipped due to missing data, {} cells skipped due to invalid heights", editorID.c_str(), precacheFallbackCount, skippedMissingCellDataCount, skippedInvalidHeightCount); @@ -607,6 +652,7 @@ bool WaterCache::TryBuildRuntimeCache(const DiskCache& diskCache, RuntimeCache& int32_t diskReadIndex = 0; int32_t skippedInvalidInstructionCount = 0; + int32_t skippedUnresolvedFormCount = 0; for (int32_t lodLevelIdx = 0; lodLevelIdx < 4; ++lodLevelIdx) { auto& lodInstructions = cache.instructions[lodLevelIdx]; @@ -643,10 +689,15 @@ bool WaterCache::TryBuildRuntimeCache(const DiskCache& diskCache, RuntimeCache& continue; } - instruction.form.ptr = RE::TESForm::LookupByID(instruction.form.id); - if (!instruction.form.ptr || instruction.form.ptr->formType != RE::FormType::Water) { - logger::warn("[Unified Water] [Cache] Failed to load WaterForm {:08X}", instruction.form.id); - return false; + instruction.form.ptr = LookupWaterForm(instruction.form.id); + if (!instruction.form.ptr) { + if (!skippedUnresolvedFormCount) { + logger::warn("[Unified Water] [Cache] Failed to load WaterForm {:08X} at LOD{} cell {},{} - skipping instruction", + instruction.form.id, lodLevel, instruction.x, instruction.y); + } + skippedUnresolvedFormCount++; + diskReadIndex++; + continue; } if (!instruction.form.ptr->IsInitialized()) { @@ -664,6 +715,10 @@ bool WaterCache::TryBuildRuntimeCache(const DiskCache& diskCache, RuntimeCache& logger::debug("[Unified Water] [Cache] Skipped {} cached instructions with invalid water heights", skippedInvalidInstructionCount); } + if (skippedUnresolvedFormCount > 1) { + logger::warn("[Unified Water] [Cache] Skipped {} cached instructions with unresolvable water forms", skippedUnresolvedFormCount); + } + return true; } From 29cb5e621a602b92a812b06c682595e037a74b56 Mon Sep 17 00:00:00 2001 From: Skrubby Skrub In A Shrub <87662196+SkrubbySkrubInAShrub@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:15:48 +0200 Subject: [PATCH 39/55] chore(tools): add RenderDoc verification harness (#2470) --- .claude/CLAUDE.md | 8 +- docs/development/shader-runtime-ab.md | 200 ++++++++++++++++ tools/taa-renderdoc-ab.py | 327 ++++++++++++++++++++++++++ 3 files changed, 533 insertions(+), 2 deletions(-) create mode 100644 docs/development/shader-runtime-ab.md create mode 100644 tools/taa-renderdoc-ab.py diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 814cbac0df..8bc6baafde 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -95,8 +95,12 @@ pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # bash: tools/v When refactoring an existing shader (especially the decompile-transcription shaders like `ISTemporalAA.hlsl`), use `tools/verify-shader-refactor.ps1` to prove the change is -behavior-preserving: identical compiled bytecode means a provable no-op. See -`docs/development/shader-workflow.md` for details. +behavior-preserving: identical compiled bytecode means a provable no-op. When the refactor +legitimately reorders ops (bytecode differs but behavior shouldn't), validate it with the runtime +A/B harness instead — capture one frame, swap just that shader, and diff the output against the +shipping baseline (`tools/taa-renderdoc-ab.py`). See `docs/development/shader-workflow.md` and +`docs/development/shader-runtime-ab.md` for details. + ### Custom CMake Targets diff --git a/docs/development/shader-runtime-ab.md b/docs/development/shader-runtime-ab.md new file mode 100644 index 0000000000..de5492ea52 --- /dev/null +++ b/docs/development/shader-runtime-ab.md @@ -0,0 +1,200 @@ +# Shader runtime A/B check + +A same-frame, GPU-level equivalence check for a shader refactor whose compiled bytecode +legitimately diverges — an op-reordering / algebraic restructure (e.g. the TAA bracket/flicker/ +blend core). For refactors that stay **bytecode-identical**, use `tools/verify-shader-refactor.ps1` +instead — a hard offline proof that needs no game. + +TAA is the worked example here, but the technique and the [Lessons](#lessons-generalizable-re-technique) +generalize to any shader RE/refactor — see [Generalizing to other shaders](#generalizing-to-other-shaders). + +## Why same-frame swap (not two-launch screenshots) + +TAA is temporal: its output depends on accumulated history. Two separate game launches never +align frame-for-frame (animated menu, HMD pose, timing/RNG), so a screenshot A/B only yields a +noisy tolerance diff. Instead we capture **one** real frame in RenderDoc and replace just the +TAA pixel shader on that captured frame. The inputs (history `t1`, velocity `t2`, depth `t3`, +mask `t4`, alpha `t5`, cbuffer `b2`) are frozen, so A (shipping) and B (candidate) run on +byte-identical inputs — a near-zero output diff means equivalent behavior on a real frame. + +This is the runtime analog of the offline verifier, tolerant of the bytecode divergence an +op-reordering restructure produces. + +## Prerequisites + +- RenderDoc, reachable via the `renderdoc` MCP (`Eval`, `Get-Texture`, `Instance`). +- SkyrimSE or SkyrimVR launchable with RenderDoc injected (`rd.ExecuteAndInject(, …, +hookIntoChildren=True)`, or launch from the RenderDoc GUI) — `` is `skse64_loader.exe` + for SE/AE and `sksevr_loader.exe` for VR. The **main menu** already runs the TAA pass, so it is + a valid capture surface on either edition. +- The build must be in **TAA upscaling mode** (DLSS/FSR replace `ISTemporalAA`) and ideally with + **HDR and frame-generation off** — otherwise Open Shaders presents through a DX12 interop + swapchain and RenderDoc captures only the D3D12 present (Copy/Present, no draws), not the + D3D11 TAA pass. +- `fxc.exe` (Windows SDK) — same compiler the offline verifier uses. + +## Steps + +### 1. Compile A' (current) and B (candidate) to DXBC — match the build's permutation + +Use the SAME defines as the running build (SkyrimSE ⇒ no `VR`; SkyrimVR ⇒ `VR`; add `HDR_OUTPUT` +only if HDR is on) and `/I package/Shaders` so includes resolve. Feeding DXBC sidesteps +RenderDoc's HLSL include/define handling. + +**A′ and B must come from different sources** — A′ from the **deployed/shipping** shader (so its +diff vs the live RT establishes the noise floor), B from your **candidate**. Pull A′ from a git +ref and B from the working tree so they can't accidentally be the same file: + +```powershell +$fxc = (Get-Command fxc.exe).Source +$inc = "package/Shaders"; $sh = "package/Shaders/ISTemporalAA.hlsl" +$defs = @("/D","PSHADER=1") # add "/D","VR=1" and/or "/D","HDR_OUTPUT=1" to match the build +# Baseline A' = the DEPLOYED shader (extract the shipping ref to a temp file; UTF-8, not PS UTF-16) +[IO.File]::WriteAllLines("$env:TEMP\taa_A.hlsl", (git show origin/dev:$sh)) +& $fxc /nologo /T ps_5_0 /E main @defs /I $inc "$env:TEMP\taa_A.hlsl" /Fo "$env:TEMP\taa_A.dxbc" +# Candidate B = the refactored working tree +& $fxc /nologo /T ps_5_0 /E main @defs /I $inc $sh /Fo "$env:TEMP\taa_B.dxbc" +``` + +**Baseline = the refactor's BASE, not blindly `origin/dev`.** If your refactor is _stacked_ on a +fix/feature not yet on `dev` (e.g. a VR fix the restructure sits on top of), compiling A′ from +`dev` makes `baseline_vs_live` measure _that fix's effect_, not the compiler noise floor — you'll +see a huge "floor" (e.g. ~0.22 instead of ~0.001) and a meaningless `EQUIVALENT`. Use the +refactor's **parent commit** (`git show :$sh`) so A′ and B differ only by the +restructure. + +**Confirm `git branch` before compiling B.** A wrong-branch compile silently builds the deployed +shader as the "candidate" and reports a meaningless `EQUIVALENT`. + +### 2. Capture a frame — MOTION matters + +Confirm with `Instance list` (the `renderdoc` MCP) that an instance shows `capture_loaded: true` +after capturing. + +- The **main menu** runs the TAA pass, but its history rectification is near-passthrough on a + static image. **A menu frame does NOT validate any change that flows through the motion- + dependent path** (reject / history reproject / clip-to-AABB) — it reads a false `EQUIVALENT`. + Use a menu frame only for changes you already know are motion-independent. _(This is how a + gate-inversion bug once slipped through: byte-for-byte `EQUIVALENT` on the menu, ~4× off under + motion — caught only by re-running on an in-game motion frame and bisecting the commits.)_ +- For anything touching the blend/reject core, capture an **in-game frame with real motion**. + Via `dev-bench` + `renderdoc`: load a light **interior** save, then `record replay` a movement + recording (continuous teleport along a path = sustained motion vectors) and fire + `TargetControl.TriggerCapture(1)` ~3 s into the replay from a **concurrent** `Eval` (the replay + call blocks for its whole duration, so the trigger must run in parallel). `camera setPov vanity` + gives orbiting motion on SE; in VR the HMD drives the camera, so **player translation is the + reliable VR motion source**. +- **Capture size is the gate.** SE in-game ≈ 1.5 GB; VR interior ≈ 0.5–5.5 GB (loads fine); VR + **exterior ≈ 14 GB and wedges/crashes RenderDoc** — stay indoors (e.g. Dragonsreach). A + corrupt/oversized `.rdc` needs a full RenderDoc relaunch, not an MCP reconnect. +- `TargetControl` sometimes returns a **stale** `NewCapture` path — verify the new `.rdc` by + timestamp/size on disk, don't trust the returned string. +- On a huge frame, a full-frame `SetFrameEvent` sweep times out the eval worker — use + `taa_candidates(reverse=True, stop_after=1)` (or `max_scan_drawcalls=N`) to scan from the end + (post-process is near there) and stop at the first match. + +### 3. Load the harness and run the A/B (via the `renderdoc` MCP `Eval`) + +Load the harness, then run the A/B. (If a later call reports the functions undefined, your MCP +uses a fresh namespace per `Eval` — see Lessons; `exec` and call in the same `Eval`.) + +```python +exec(open(r"/tools/taa-renderdoc-ab.py").read()) # = your local checkout path +taa_candidates() # exhaustive forward scan — fine for menu / small captures +# On a multi-GB in-game capture, scan from the end and stop at the first hit so the worker +# doesn't time out (the TAA pass is post-process, near the end): +taa_candidates(reverse=True, stop_after=1) # or max_scan_drawcalls=N to cap the probe +``` + +(Finds ISTemporalAA by its 6-SRV fingerprint — `t0..t5`: current/history/velocity/depth/mask/alpha.) + +Pick the `eventId` of the TAA draw from the candidate list, then: + +```python +ab(, + candidate_dxbc=r"%TEMP%/taa_B.dxbc", + baseline_dxbc=r"%TEMP%/taa_A.dxbc") +``` + +### 4. Interpret + +**Always pass `baseline_dxbc`** — the verdict is _relative to it_. The game compiles the live +shader with slightly different optimization than offline fxc, so even an identical shader leaves +a small **noise floor** (`baseline_vs_live.mean_abs`, e.g. ~2e-4 on a 10-bit RT). `ab()` reports: + +- `noise_floor_mean` — the baseline residue. +- `verdict`: `EQUIVALENT` if `candidate_vs_live.mean_abs ≤ 3× the floor`, else `DIFFERS`. + +Validated on the SE main menu (TAA on, HDR/frame-gen off): the pre-rename `dev` shader read +`EQUIVALENT` (mean = floor), a deliberately ×0.5 shader read `DIFFERS` (mean ≈ 83× floor, 61% +of samples). If `baseline_vs_live.mean_abs` is _large_ (not a tiny floor), the permutation is +wrong (e.g. `HDR_OUTPUT` mismatch) — fix step 1 before trusting the candidate. + +### 5. (Optional) visual evidence + +Use the `Get-Texture` MCP tool on the output RT before vs after replacement (and over the +`x≈0.5` eye-seam region where the original VR bug showed) with `diff_amplify` for a diff image +in the report. + +## Generalizing to other shaders + +TAA is just the worked example — `grab_rt()`, `replace_ps_with_dxbc()`, `restore()`, `_diff()`, and +`ab(eid, …)` are all **shader-agnostic** (give `ab()` an event ID and it validates any pass). The +only TAA-specific piece is the fingerprint used to _find_ the pass, and that's a parameter: + +```python +# Generic finder — match a pass by the SRV names its pixel shader binds (case-insensitive): +cands = find_candidates({"mycolortex", "mydepthtex", "myhistorytex"}, reverse=True, stop_after=1) +ab(cands[0]["eventId"], candidate_dxbc=..., baseline_dxbc=...) +# taa_candidates() is just find_candidates(_TAA_TEX) — copy that one-liner for your own preset. +``` + +So to point it at a different shader you change only: + +- **The pass fingerprint** — pass your shader's distinctive SRV names to `find_candidates()` (no + source edit needed). Match by bound resources, not draw index (indices shift frame to frame). +- **The permutation defines** — compile A′/B with the exact `/D` set the running build used. +- **The capture state** — pick a frame that actually exercises the code path (see lessons). + +## Lessons (generalizable RE technique) + +These transfer to any GPU-shader RE/validation work, not just TAA: + +- **Offline byte-identity beats any runtime check — try it first.** Far more refactoring stays + bytecode-identical than you'd expect (even destructuring deeply-aliased decompile scratch, since + the compiler already SSA's it). Prove those with an `fxc` byte-compare + (`verify-shader-refactor.ps1`), one small step at a time — no game needed. Reserve a runtime swap + for changes where the compiler legitimately emits different-but-equivalent code (op reordering, + algebraic rewrites). +- **Freeze inputs with a same-frame swap.** Any pass whose inputs are bound resources can be + validated by replacing only that shader on one captured frame: A and B then run on byte-identical + inputs, so the output diff is pure shader behaviour — no cross-launch timing/RNG/pose noise. +- **Judge against a baseline noise floor, not zero.** The runtime compiler differs from offline + `fxc` by a few LSBs, so even an identical shader isn't bit-exact vs the live RT. Always swap the + _shipping_ shader in too and measure the candidate relative to that floor. +- **Capture a state that exercises the path under test.** A degenerate frame (idle menu, static + camera, untaken branch) gives a false pass for any state-dependent code — temporal history, + motion vectors, conditionally-enabled features. Identify what state your code depends on and + capture a frame in it. _(TAA: a static menu read `EQUIVALENT` even with a real motion-path bug; + only an in-game motion frame exposed it.)_ +- **Baseline against the change's true parent, not a distant branch.** If your work is stacked on + an unmerged fix, diffing against `dev` folds that fix into the "floor" → a huge bogus residue and + a meaningless verdict. Compile the baseline from the immediate parent commit. +- **Confirm which source you compiled.** A wrong-branch/ref compile silently validates the deployed + shader against itself and always reports `EQUIVALENT`. Check `git branch`/the ref first. +- **Find the pass by its resource fingerprint, and reverse-scan.** Match a draw by its bound + textures/buffers, not its index (indices shift frame to frame). Post-process sits near frame end, + so scan drawcalls in reverse — a full-frame state sweep can time out on a large capture. +- **Interop swapchains hide the inner API.** When an app presents D3D11 work through a DX12 interop + swapchain (here: HDR / frame-gen / upscaler paths), RenderDoc hooks the D3D12 present and the + D3D11 draws aren't in the frame at all. Disable the feature that triggers interop to capture the + inner API; if you can't, fall back to offline byte-identity for that permutation. +- **Captures scale with scene complexity.** A heavy scene (especially stereo/VR) can produce a + multi-GB capture that wedges the tools; shrink the scene (interior, lower res) to keep captures + loadable. Verify a new capture by file timestamp/size — capture APIs sometimes return a stale path. +- **Know your tooling's quirks.** (Here, the `renderdoc` MCP `Eval`: a fresh namespace per call can + drop your defs — `exec` the harness and call it in the _same_ `Eval`; and stdout may lag one call + behind, so poll again to drain it.) +- One frame proves equivalence on that frame; loop over a few representative frames for confidence. + This is a regression check against a known-good baseline, not a formal proof — pair it with the + offline verifier, which formally covers everything that stays bytecode-identical. diff --git a/tools/taa-renderdoc-ab.py b/tools/taa-renderdoc-ab.py new file mode 100644 index 0000000000..cd15c272ce --- /dev/null +++ b/tools/taa-renderdoc-ab.py @@ -0,0 +1,327 @@ +# taa-renderdoc-ab.py — runtime A/B check for TAA shader refactors. +# +# Purpose: prove a behavior-preserving claim that bytecode-identity CANNOT (an op-reordering +# restructure, where fxc legitimately emits different-but-equivalent code). It swaps the ISTemporalAA +# pixel shader on a SINGLE captured frame and diffs the output render target. Because the +# inputs (history t1, velocity t2, depth t3, mask t4, alpha t5, cbuffer b2) are frozen from +# that one frame, A (shipping shader) and B (candidate) run on byte-identical inputs — so a +# near-zero output diff means equivalent behavior on a real frame, free of the cross-launch +# and temporal-warmup noise that a two-launch screenshot A/B suffers. +# +# HOW TO RUN: this is executed *inside RenderDoc's embedded Python* via the renderdoc MCP +# `Eval` tool (the global `ctx` HandlerContext must be in scope). It is NOT a standalone +# script. See docs/development/shader-runtime-ab.md for the full operator runbook. +# +# Candidate B (and a baseline A') are supplied as pre-compiled DXBC built with fxc using the +# SAME defines/permutation as the captured build (e.g. PSHADER VR [HDR_OUTPUT]) and with +# /I package/Shaders so includes resolve — identical to tools/verify-shader-refactor.ps1. +# Feeding DXBC (ShaderEncoding.DXBC) avoids RenderDoc's HLSL include/define handling. + +import renderdoc as rd +import struct +import os +import math + +# eid -> (origPS ResourceId, replacement ResourceId), kept so restore() uses the real objects +# instead of round-tripping through strings. Lives as long as this module's namespace does — if +# your MCP gives a fresh namespace per Eval, do the replace and its restore in the SAME Eval. +_replacements = {} + +# Above this baseline-vs-live mean_abs, the floor is implausible (an honest one is a few LSBs): +# the baseline diverged from live, so ab() reports UNVERIFIED-BASELINE instead of judging against +# it. Tune to the observed runtime-vs-fxc residue for your RT format. +FLOOR_SANITY_LIMIT = 1e-2 + + +def _walk(actions): + for a in actions: + yield a + for c in _walk(a.children): + yield c + + +def _desc_res(d): + for attr in ("resource", "resourceId"): + if hasattr(d, attr): + return getattr(d, attr) + return rd.ResourceId.Null + + +def _as_resid(x): + # GetShader returns a ResourceId on current RenderDoc; guard older (id, reflection) tuples. + return x[0] if isinstance(x, tuple) else x + + +# ISTemporalAA's pixel shader binds exactly these t0..t5 textures — a reliable fingerprint. +# (Output-slot counting is unreliable: D3D11 returns 8 RTV slots and empty ones read as +# "ResourceId::0", which does NOT compare equal to rd.ResourceId.Null.) +_TAA_TEX = {"currentframetex", "historytex", "velocitytex", "depthtex", "masktex", "alphatex"} + + +def find_candidates(srv_names, reverse=False, max_scan_drawcalls=0, stop_after=0): + """Find draws whose pixel shader binds ALL of `srv_names` — a resource-fingerprint match that + is robust across frames/versions (unlike a draw index, which shifts). `srv_names` is any + iterable of SRV names (case-insensitive). Returns [{eventId, name}]; pass an eventId to ab(). + This is the shader-agnostic finder — `ab()`/`grab_rt()`/`replace_ps_with_dxbc()` are already + generic, so this plus your shader's fingerprint is all you need to validate a different pass. + + A full forward scan calls SetFrameEvent() once per drawcall, which can time out the replay + worker on a multi-GB capture. Post-process passes sit near frame end, so pass reverse=True to + scan from the end and/or max_scan_drawcalls=N to cap how many drawcalls are probed; stop_after=N + returns as soon as N matches are found. Defaults keep the exhaustive forward scan.""" + want = {s.lower() for s in srv_names} + def work(ctrl): + sdfile = ctrl.GetStructuredFile() + draws = [a for a in _walk(ctrl.GetRootActions()) if (a.flags & rd.ActionFlags.Drawcall)] + if reverse: + draws = draws[::-1] + if max_scan_drawcalls > 0: + draws = draws[:max_scan_drawcalls] + res = [] + for a in draws: + ctrl.SetFrameEvent(a.eventId, True) + refl = ctrl.GetPipelineState().GetShaderReflection(rd.ShaderStage.Pixel) + if not refl: + continue + names = {r.name.lower() for r in refl.readOnlyResources} + if want.issubset(names): + res.append({"eventId": a.eventId, "name": a.GetName(sdfile)}) + if stop_after > 0 and len(res) >= stop_after: + break + return res + return ctx.replay(work) + + +def taa_candidates(reverse=False, max_scan_drawcalls=0, stop_after=0): + """TAA preset: find_candidates() with ISTemporalAA's t0..t5 SRV fingerprint (_TAA_TEX).""" + return find_candidates(_TAA_TEX, reverse=reverse, + max_scan_drawcalls=max_scan_drawcalls, stop_after=stop_after) + + +def _tex_desc(ctrl, rid): + for t in ctrl.GetTextures(): + if t.resourceId == rid: + return t + return None + + +def grab_rt(eid, target_index=0): + """Return (resourceId_str, raw_bytes, (width, height, format_name)) for an output RT.""" + def work(ctrl): + ctrl.SetFrameEvent(eid, True) + outs = ctrl.GetPipelineState().GetOutputTargets() + # Fail soft on an out-of-range or empty (ResourceId::0) slot rather than letting + # GetTextureData raise and abort the whole Eval session. + if target_index >= len(outs): + return (None, b"", (0, 0, "none")) + rid = _desc_res(outs[target_index]) + if str(rid) == "ResourceId::0": + return (str(rid), b"", (0, 0, "none")) + data = bytes(ctrl.GetTextureData(rid, rd.Subresource(0, 0, 0))) + td = _tex_desc(ctrl, rid) + meta = (td.width, td.height, str(td.format.Name())) if td else (0, 0, "?") + return (str(rid), data, meta) + return ctx.replay(work) + + +def replace_ps_with_dxbc(eid, dxbc_path, entry="main"): + """Build a replacement PS from a pre-compiled DXBC file and bind it at the event. + Returns {ok, errors}. On success the (orig, new) ResourceIds are stored for restore(eid).""" + # Expand %TEMP%/~ and fail soft on a missing file — a raw open() would abort the whole Eval. + p = os.path.expandvars(os.path.expanduser(dxbc_path)) + if not os.path.isfile(p): + return {"ok": False, "errors": "DXBC not found: " + p} + try: + with open(p, "rb") as f: + blob = f.read() + except OSError as e: + return {"ok": False, "errors": "cannot read %s: %r" % (p, e)} + + # Undo any prior replacement on this event first, so repeated swaps don't leak target + # resources or stack replacements (orig would otherwise capture an already-replaced shader). + if eid in _replacements: + restore(eid) + + def work(ctrl): + ctrl.SetFrameEvent(eid, True) + orig = _as_resid(ctrl.GetPipelineState().GetShader(rd.ShaderStage.Pixel)) + newid, errs = ctrl.BuildTargetShader(entry, rd.ShaderEncoding.DXBC, blob, + rd.ShaderCompileFlags(), rd.ShaderStage.Pixel) + ok = newid != rd.ResourceId.Null + if ok: + ctrl.ReplaceResource(orig, newid) + ctrl.SetFrameEvent(eid, True) # re-replay with replacement bound + return ok, orig, newid, str(errs) + + ok, orig, newid, errs = ctx.replay(work) + if ok: + _replacements[eid] = (orig, newid) + return {"ok": ok, "errors": errs} + + +def restore(eid): + """Undo a replacement made at eid, using the stored ResourceIds.""" + pair = _replacements.pop(eid, None) + if not pair: + return False + orig, newid = pair + + def work(ctrl): + ctrl.RemoveReplacement(orig) + try: + ctrl.FreeTargetResource(newid) + except Exception: + pass + ctrl.SetFrameEvent(eid, True) + return True + return ctx.replay(work) + + +def _diff(a, b, meta): + """Byte-identity fast path + a SAMPLED float-magnitude estimate (no numpy in RenderDoc, + and full-res TAA RTs are tens of MB — never materialize the whole thing).""" + # Always populate mean_abs/max_abs so callers (ab) can read them unconditionally. + out = {"size_a": len(a), "size_b": len(b), "format": meta[2], "dims": [meta[0], meta[1]], + "mean_abs": None, "max_abs": None} + if not a or not b: # fail-soft grab_rt returned empty bytes — not comparable + out["verdict"] = "NO-DATA" + return out + if a == b: # C-level compare — instant + out["verdict"] = "IDENTICAL" + out["bytes_differing"] = 0 + out["mean_abs"] = 0.0 + out["max_abs"] = 0.0 + out["sample_frac_differing"] = 0.0 + return out + if len(a) != len(b): + out["verdict"] = "DIFFERS" + out["note"] = "size mismatch" # mean_abs stays None -> ab() treats as not-comparable + return out + + # Per-sample delta units by format: UNORM8 and packed R10G10B10A2 are normalized to [0,1] + # (the packed format MUST be unpacked by channel, not byte — a 1-LSB 10-bit delta spans byte + # boundaries and a byte-diff hugely overstates it); float16/float32 are compared in their + # native units (abs(x-y)). mean_abs is therefore only comparable within one format, which is + # fine here since baseline and candidate always share the live RT's format. No verdict here: + # ab() judges relative to the baseline noise floor, because the game's runtime compile differs + # from offline fxc by a few LSBs even for an identical shader (that residue is the floor). + fmt = meta[2].lower() + packed = "10g10b10a2" in fmt + if packed: + esz, code = 4, None + elif "16_float" in fmt: + esz, code = 2, "> s) & m) / d - ((py >> s) & m) / d) for (s, m, d) in A2) + elif code: + x = struct.unpack_from(code, a, off)[0] + y = struct.unpack_from(code, b, off)[0] + # NaN/inf-safe: abs(NaN) AND inf-inf both yield NaN, which silently fails every + # comparison below and could fake EQUIVALENT. Treat equal values (incl. both-NaN and + # both-inf) as 0; any NaN/inf-vs-finite mismatch as a max difference. + dlt = abs(x - y) + if math.isnan(dlt): + dlt = 0.0 if (x == y or (math.isnan(x) and math.isnan(y))) else float("inf") + else: + dlt = abs(a[off] - b[off]) / 255.0 + if dlt > 0: + ndiff += 1 + if dlt > maxabs: + maxabs = dlt + absSum += dlt + cnt += 1 + + out["sampled_elems"] = cnt + out["sample_frac_differing"] = (ndiff / cnt) if cnt else 0.0 + out["max_abs"] = maxabs + out["mean_abs"] = (absSum / cnt) if cnt else 0.0 + out["verdict"] = "COMPARED" # metrics present; ab() assigns the final baseline-relative verdict + return out + + +def ab(eid, candidate_dxbc, baseline_dxbc=None, entry="main"): + """Full A/B on one event. + - Captures the live output (A_real). + - If baseline_dxbc given: swap it in and confirm it matches A_real (validates defines/ + permutation + the DXBC-replace path) before trusting the candidate result. + - Swaps candidate_dxbc and diffs against A_real. Restores after each swap. + A failed build is reported as BUILD-FAILED — never a false EQUIVALENT (which would happen + if we diffed with the original shader still bound).""" + rid, a_real, meta = grab_rt(eid) + report = {"eventId": eid, "rt": rid, "rt_meta": {"dims": [meta[0], meta[1]], "format": meta[2]}} + + if baseline_dxbc: + r = replace_ps_with_dxbc(eid, baseline_dxbc, entry) + if not r["ok"]: + report["baseline_vs_live"] = {"verdict": "BUILD-FAILED", "errors": r["errors"]} + else: + # finally: guarantee the replacement is undone even if grab/diff raises, so a stale + # shader can't contaminate later replays. + try: + _, a_prime, _ = grab_rt(eid) + report["baseline_vs_live"] = _diff(a_real, a_prime, meta) + finally: + restore(eid) + + r = replace_ps_with_dxbc(eid, candidate_dxbc, entry) + if not r["ok"]: + report["candidate_vs_live"] = {"verdict": "BUILD-FAILED", "errors": r["errors"]} + report["verdict"] = "BUILD-FAILED" + return report + try: + _, b, _ = grab_rt(eid) + report["candidate_vs_live"] = _diff(a_real, b, meta) + finally: + restore(eid) + + # Verdict: judge the candidate's mean_abs against the baseline noise floor (runtime-vs-fxc + # compile residue). EQUIVALENT if within 3x the floor; DIFFERS otherwise. + base = report.get("baseline_vs_live") or {} + floor = base.get("mean_abs") + cand_mean = report["candidate_vs_live"].get("mean_abs") + if cand_mean is None: + # candidate output not comparable (empty RT / size mismatch) — surface, don't crash. + report["verdict"] = "NOT-COMPARABLE" + elif floor is not None: + report["noise_floor_mean"] = floor + if floor > FLOOR_SANITY_LIMIT: + # An honest floor is a few LSBs of runtime-vs-fxc residue. A large floor means the + # baseline diverged from live — wrong defines/permutation, or a baseline compiled from + # the wrong ref (e.g. dev when the refactor is stacked on an unmerged fix). Judging + # against it would rubber-stamp almost any candidate EQUIVALENT, so refuse to judge. + report["verdict"] = "UNVERIFIED-BASELINE" + report["note"] = ("baseline noise floor %.4g exceeds %.4g — verify defines/permutation " + "and that the baseline is the refactor's parent" % (floor, FLOOR_SANITY_LIMIT)) + else: + report["verdict"] = "EQUIVALENT" if cand_mean <= max(floor * 3.0, 1e-4) else "DIFFERS" + elif baseline_dxbc: + # A baseline was requested but did not yield a usable floor (build failed or size + # mismatch). Do NOT silently fall back to the absolute path — the floor is the whole + # point of passing a baseline, so the candidate is unjudged. + report["verdict"] = "UNVERIFIED-BASELINE" + else: + # No baseline requested: best-effort absolute tolerance only. + report["verdict"] = "EQUIVALENT" if cand_mean <= 1e-3 else "DIFFERS" + return report From b48bf2409688ef863aa7510f44d0e6c354fd925d Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:48:48 +0100 Subject: [PATCH 40/55] chore: remove all VR support (#2475) Co-authored-by: Claude Sonnet 4.5 --- .claude/CLAUDE.md | 52 +- .coderabbit.yaml | 2 +- .github/configs/README.md | 12 +- .github/configs/generate-shader-configs.ps1 | 26 +- .github/configs/shader-validation-vr.yaml | 23410 ---------------- .github/copilot-instructions.md | 8 +- .github/workflows/_shared-build.yaml | 2 - AI-INSTRUCTIONS.md | 8 +- CMakePresets.json | 4 +- CMakeUserPresets.json.template | 2 +- README.md | 2 - containerbuild.ps1 | 2 +- docs/development/README.md | 2 +- docs/development/shader-workflow.md | 2 +- docs/new-feature-template/NewFeature.h | 1 - docs/new-feature-template/NewFeatureReadme.md | 10 +- .../DynamicCubemaps/UpdateCubemapCS.hlsl | 6 +- .../ExponentialHeightFog.hlsli | 46 +- .../VolumetricFogCSCommon.hlsli | 11 +- .../VolumetricFogConservativeDepthCS.hlsl | 10 +- .../VolumetricFogIntegrationCS.hlsl | 6 +- .../VolumetricFogLightScatteringCS.hlsl | 58 +- .../VolumetricFogMaterialCS.hlsl | 5 +- .../ExtendedMaterials/ExtendedMaterials.hlsli | 25 +- .../GrassCollision/GrassCollision.hlsli | 4 +- .../Hair Specular/Shaders/Hair/Hair.hlsli | 10 +- .../LightLimitFix/ClusterBuildingCS.hlsl | 9 +- .../LightLimitFix/ClusterCullingCS.hlsl | 10 +- .../Shaders/LightLimitFix/Common.hlsli | 2 +- .../Shaders/ScreenSpaceGI/blur.cs.hlsl | 26 +- .../Shaders/ScreenSpaceGI/common.hlsli | 13 +- .../Shaders/ScreenSpaceGI/gi.cs.hlsl | 36 +- .../ScreenSpaceGI/prefilterDepths.cs.hlsl | 4 - .../ScreenSpaceGI/radianceDisocc.cs.hlsl | 30 +- .../Shaders/ScreenSpaceGI/stereoSync.cs.hlsl | 122 - .../ScreenSpaceShadows/RaymarchCS.hlsl | 5 - .../ScreenSpaceShadows.hlsli | 2 +- .../ScreenSpaceShadows/StereoSyncCS.hlsl | 169 - .../ScreenSpaceShadows/bend_sss_gpu.hlsli | 40 - .../Shaders/SubsurfaceScattering/Burley.hlsli | 6 +- .../SubsurfaceScattering/SeparableSSSCS.hlsl | 3 +- .../Shaders/Upscaling/ClearHMDMaskCS.hlsl | 23 - .../Upscaling/DepthRefractionUpscalePS.hlsl | 28 +- .../Shaders/Upscaling/EncodeTexturesCS.hlsl | 33 +- .../Upscaling/UnderwaterMaskUpscalePS.hlsl | 92 - features/VR/CORE | 0 features/VR/Shaders/Features/VR.ini | 2 - .../VolumetricShadows/VolumetricShadows.hlsli | 14 +- .../Shaders/WaterEffects/WaterCaustics.hlsli | 4 +- .../CommunityShaders/Overrides/README.md | 1 - .../CommunityShaders/Translations/en.json | 41 +- .../CommunityShaders/Translations/zh_CN.json | 39 +- package/Shaders/Common/FrameBuffer.hlsli | 100 +- package/Shaders/Common/MotionBlur.hlsli | 6 +- package/Shaders/Common/ShadowSampling.hlsli | 12 +- package/Shaders/Common/SharedData.hlsli | 27 +- package/Shaders/Common/VR.hlsli | 668 - package/Shaders/DeferredCompositeCS.hlsl | 38 +- package/Shaders/DistantTree.hlsl | 74 +- package/Shaders/Effect.hlsl | 149 +- .../Shaders/ISApplyVolumetricLighting.hlsl | 31 +- package/Shaders/ISFullScreenVR.hlsl | 69 - package/Shaders/ISReflectionsRayTracing.hlsl | 54 +- package/Shaders/ISSAOComposite.hlsl | 17 +- package/Shaders/ISSAOMinify.hlsl | 2 +- package/Shaders/ISTemporalAA.hlsl | 4 - .../ISVolumetricLightingGenerateCS.hlsl | 43 +- package/Shaders/ISWaterBlend.hlsl | 9 +- package/Shaders/Lighting.hlsl | 237 +- package/Shaders/Particle.hlsl | 58 +- package/Shaders/RunGrass.hlsl | 146 +- package/Shaders/Sky.hlsl | 64 +- package/Shaders/Tests/TestVR.hlsl | 362 - package/Shaders/Tests/TestVRFlat.hlsl | 90 - package/Shaders/Utility.hlsl | 114 +- package/Shaders/VR/InSceneOverlay.ps.hlsl | 18 - package/Shaders/VR/InSceneOverlay.vs.hlsl | 27 - package/Shaders/VR/StereoBlendCS.hlsl | 362 - package/Shaders/VR/VRPostProcessCS.hlsl | 109 - .../VRStereoOptimizations/StencilCS.hlsl | 172 - .../VRStereoOptimizations/StencilWritePS.hlsl | 40 - .../VRStereoOptimizations/StencilWriteVS.hlsl | 24 - .../VRStereoOptimizations/cbuffers.hlsli | 31 - .../Shaders/VRStereoOptimizations/modes.hlsli | 10 - package/Shaders/Water.hlsl | 201 +- src/CSEditor/Weather/PrecipitationWidget.cpp | 4 +- src/Deferred.cpp | 96 +- src/Deferred.h | 9 +- .../ShadowmapCascadeCullingFix.cpp | 2 +- .../ShadowmapCascadeRasterizerFix.cpp | 2 +- .../ShadowmapCascadeRasterizerFix.h | 2 +- src/Feature.cpp | 30 +- src/Feature.h | 6 - src/FeatureConstraints.h | 2 +- src/FeatureIssues.cpp | 6 - src/FeatureIssues.h | 2 +- src/Features/CSEditor.cpp | 2 +- src/Features/CSEditor.h | 1 - src/Features/CloudShadows.cpp | 2 +- src/Features/CloudShadows.h | 1 - src/Features/DynamicCubemaps.cpp | 46 +- src/Features/DynamicCubemaps.h | 19 +- src/Features/ExponentialHeightFog.cpp | 11 +- src/Features/ExponentialHeightFog.h | 3 +- src/Features/ExtendedMaterials.h | 1 - src/Features/ExtendedTranslucency.h | 1 - src/Features/GrassCollision.cpp | 17 +- src/Features/GrassCollision.h | 3 +- src/Features/GrassLighting.h | 1 - src/Features/HDRDisplay.cpp | 56 +- src/Features/HDRDisplay.h | 1 - src/Features/HairSpecular.h | 1 - src/Features/IBL.h | 1 - src/Features/InteriorSun.cpp | 10 +- src/Features/InteriorSun.h | 1 - src/Features/InverseSquareLighting.h | 1 - src/Features/LODBlending.h | 1 - src/Features/LightLimitFix.cpp | 48 +- src/Features/LightLimitFix.h | 15 +- src/Features/LinearLighting.cpp | 2 +- src/Features/LinearLighting.h | 1 - src/Features/PerformanceOverlay.h | 1 - src/Features/RenderDoc.h | 1 - src/Features/ScreenSpaceGI.cpp | 72 +- src/Features/ScreenSpaceGI.h | 17 +- src/Features/ScreenSpaceShadows.cpp | 166 +- src/Features/ScreenSpaceShadows.h | 22 +- src/Features/ScreenshotFeature.cpp | 45 +- src/Features/ScreenshotFeature.h | 3 +- src/Features/Skin.h | 1 - src/Features/SkySync.h | 1 - src/Features/Skylighting.cpp | 27 +- src/Features/Skylighting.h | 7 - src/Features/SubsurfaceScattering.cpp | 2 +- src/Features/SubsurfaceScattering.h | 1 - src/Features/TerrainBlending.cpp | 52 +- src/Features/TerrainBlending.h | 5 +- src/Features/TerrainHelper.cpp | 3 +- src/Features/TerrainHelper.h | 1 - src/Features/TerrainShadows.h | 1 - src/Features/TerrainVariation.h | 1 - src/Features/UnifiedWater.cpp | 2 +- src/Features/UnifiedWater.h | 1 - src/Features/Upscaling.cpp | 600 +- src/Features/Upscaling.h | 46 +- src/Features/Upscaling/DX12SwapChain.cpp | 8 +- src/Features/Upscaling/FidelityFX.cpp | 200 +- src/Features/Upscaling/RCAS/RCAS.cpp | 4 +- src/Features/Upscaling/Streamline.cpp | 171 +- src/Features/Upscaling/Streamline.h | 5 +- src/Features/VR.cpp | 294 - src/Features/VR.h | 558 - src/Features/VR/InSceneOverlay.cpp | 612 - src/Features/VR/Input.cpp | 383 - src/Features/VR/OpenVRDetection.cpp | 136 - src/Features/VR/OpenVRDetection.h | 48 - src/Features/VR/OverlayDrag.cpp | 405 - src/Features/VR/SettingsUI.cpp | 1139 - src/Features/VR/StereoBlend.cpp | 227 - src/Features/VR/WandPointing.cpp | 146 - src/Features/VRStereoOptimizations.cpp | 622 - src/Features/VRStereoOptimizations.h | 221 - src/Features/VolumetricLighting.cpp | 72 +- src/Features/VolumetricLighting.h | 36 - src/Features/VolumetricShadows.cpp | 2 +- src/Features/VolumetricShadows.h | 1 - src/Features/WaterEffects.h | 1 - src/Features/WetnessEffects.h | 1 - src/FrameAnnotations.cpp | 108 +- src/Globals.cpp | 128 +- src/Globals.h | 109 +- src/Hooks.cpp | 69 +- src/Menu.cpp | 31 +- src/Menu.h | 4 +- src/Menu/HomePageRenderer.cpp | 5 - src/Menu/OverlayRenderer.cpp | 17 - src/Menu/OverlayRenderer.h | 5 +- src/Menu/SettingsTabRenderer.cpp | 1 - src/Menu/ThemeManager.cpp | 11 +- src/ShaderCache.cpp | 79 +- src/ShaderCache.h | 90 +- src/State.cpp | 37 +- src/State.h | 3 +- src/TruePBR.cpp | 2 +- src/TruePBR.h | 1 - src/Utils/D3D.cpp | 10 +- src/Utils/D3D.h | 11 - src/Utils/Game.cpp | 31 +- src/Utils/Game.h | 30 +- src/Utils/GameSetting.h | 4 +- src/Utils/Input.h | 97 +- src/Utils/UI.cpp | 58 - src/Utils/UI.h | 6 +- src/Utils/VRUtils.cpp | 241 - src/Utils/VRUtils.h | 248 - src/XSEPlugin.cpp | 15 +- tools/verify-shader-refactor.ps1 | 8 +- 197 files changed, 970 insertions(+), 35114 deletions(-) delete mode 100644 .github/configs/shader-validation-vr.yaml delete mode 100644 features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl delete mode 100644 features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl delete mode 100644 features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl delete mode 100644 features/VR/CORE delete mode 100644 features/VR/Shaders/Features/VR.ini delete mode 100644 package/Shaders/Common/VR.hlsli delete mode 100644 package/Shaders/ISFullScreenVR.hlsl delete mode 100644 package/Shaders/Tests/TestVR.hlsl delete mode 100644 package/Shaders/Tests/TestVRFlat.hlsl delete mode 100644 package/Shaders/VR/InSceneOverlay.ps.hlsl delete mode 100644 package/Shaders/VR/InSceneOverlay.vs.hlsl delete mode 100644 package/Shaders/VR/StereoBlendCS.hlsl delete mode 100644 package/Shaders/VR/VRPostProcessCS.hlsl delete mode 100644 package/Shaders/VRStereoOptimizations/StencilCS.hlsl delete mode 100644 package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl delete mode 100644 package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl delete mode 100644 package/Shaders/VRStereoOptimizations/cbuffers.hlsli delete mode 100644 package/Shaders/VRStereoOptimizations/modes.hlsli delete mode 100644 src/Features/VR.cpp delete mode 100644 src/Features/VR.h delete mode 100644 src/Features/VR/InSceneOverlay.cpp delete mode 100644 src/Features/VR/Input.cpp delete mode 100644 src/Features/VR/OpenVRDetection.cpp delete mode 100644 src/Features/VR/OpenVRDetection.h delete mode 100644 src/Features/VR/OverlayDrag.cpp delete mode 100644 src/Features/VR/SettingsUI.cpp delete mode 100644 src/Features/VR/StereoBlend.cpp delete mode 100644 src/Features/VR/WandPointing.cpp delete mode 100644 src/Features/VRStereoOptimizations.cpp delete mode 100644 src/Features/VRStereoOptimizations.h delete mode 100644 src/Utils/VRUtils.cpp delete mode 100644 src/Utils/VRUtils.h diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8bc6baafde..8fc8743987 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -21,12 +21,10 @@ powershell.exe -Command "./BuildRelease.bat [PRESET_NAME]" **Available Presets** (from CMakePresets.json): -- `ALL` (default) - Builds universal binary supporting SE/AE/VR runtime detection +- `ALL` (default) - Builds universal binary supporting SE/AE runtime detection - `SE` - Skyrim Special Edition only (compile-time targeting) - `AE` - Anniversary Edition only (compile-time targeting) -- `VR` - Skyrim VR only (compile-time targeting) -- `PRE-AE` - SE + VR (excludes AE) -- `FLATRIM` - SE + AE (excludes VR) +- `FLATRIM` - SE + AE - `ALL-TRACY` - Universal binary with Tracy profiler support enabled **User Preset Template**: @@ -51,7 +49,7 @@ powershell.exe -Command "./BuildRelease.bat [PRESET_NAME]" Set `CommunityShadersOutputDir` environment variable to semicolon-separated Skyrim Data directories: ``` -CommunityShadersOutputDir=F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data +CommunityShadersOutputDir=F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data ``` ### Shader Development and Testing @@ -66,9 +64,6 @@ cmake --build ./build/ALL --target prepare_shaders # Full shader suite validation (can be time-consuming) hlslkit-compile --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml --max-warnings 0 --suppress-warnings X1519 -# VR-specific validation -hlslkit-compile --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config .github/configs/shader-validation-vr.yaml --max-warnings 0 --suppress-warnings X1519 - # Targeted testing for faster development (recommended during development) # Test specific base shader hlslkit-compile --shader-dir build/ALL/aio/Shaders/Lighting.hlsl --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml @@ -89,7 +84,7 @@ hlslkit-generate-defines --log CommunityShaders.log hlslkit-buffer-scan --features-dir features/ # Prove a shader refactor changed no behavior (compiles base ref vs working tree, -# compares DXBC across VR x HDR_OUTPUT permutations; exit 0 identical / 2 differs) +# compares DXBC across HDR_OUTPUT permutations; exit 0 identical / 2 differs) pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # bash: tools/verify-shader-refactor.sh ``` @@ -201,8 +196,7 @@ Each feature follows consistent structure: ### Cross-Platform Support -**Single Binary**: Supports SE/AE/VR through CommonLibSSE-NG runtime detection -**VR Adaptations**: Specialized rendering paths in `src/Features/VR/` +**Single Binary**: Supports SE/AE through CommonLibSSE-NG runtime detection **API Abstraction**: Dual DirectX 11 support with feature-specific rendering strategies ## Critical Dependencies @@ -232,20 +226,11 @@ CommonLibSSE-NG supports multiple Skyrim versions through sophisticated runtime - `SE` - Skyrim Special Edition only - `AE` - Anniversary Edition only -- `VR` - Skyrim VR only -- `ALL` - Multi-runtime support (default for this project) +- `ALL` - Multi-runtime SE/AE support (default for this project) **Compile-Time vs Runtime Patterns**: -**Single Runtime (compile-time)**: When targeting one version, `#ifdef ENABLE_SKYRIM_VR` conditionally compiles VR-specific code: - -```cpp -#ifdef ENABLE_SKYRIM_VR - virtual void Unk_09(UI_MENU_Unk09 a_unk); // VR-only vfunc -#endif -``` - -**Multi-Runtime (runtime detection)**: When targeting ALL, uses runtime accessors: +**Multi-Runtime (runtime detection)**: When targeting ALL, uses runtime accessors for SE vs AE differences: ```cpp // Runtime member access with different offsets per version @@ -253,26 +238,16 @@ auto& GetRuntimeData() { return REL::RelocateMemberIfNewer( SKSE::RUNTIME_SSE_1_6_629, this, 0x3D8, 0x3E0); } - -// VR-specific runtime data (only exists in VR) -auto& GetVRRuntimeData() { - return REL::RelocateMember(this, 0, 0x3D8); -} - -// Runtime detection -if (REL::Module::IsVR()) { - // VR-specific code path -} ``` **Key Runtime Utilities**: - `REL::RelocateMember()` - Access members with different offsets - `REL::RelocateVirtual()` - Call virtual functions with variant vtables -- `REL::Module::IsVR()`, `IsAE()`, `IsSE()` - Runtime version detection +- `REL::Module::IsAE()`, `IsSE()` - Runtime version detection - `REL::RelocationID()` - Dynamic address resolution based on version -**Critical for Development**: When modifying classes that inherit from game objects, always check if they have runtime-specific variations and use appropriate accessor patterns. +**Critical for Development**: When modifying classes that inherit from game objects, always check if they have runtime-specific variations (SE vs AE) and use appropriate accessor patterns. ## Core Architecture @@ -302,7 +277,6 @@ All graphics features are globally accessible for cross-feature coordination: - Materials: `extendedMaterials`, `hairSpecular`, `subsurfaceScattering` - Effects: `screenSpaceGI`, `screenSpaceShadows`, `waterEffects`, `wetnessEffects` - Environment: `cloudShadows`, `dynamicCubemaps`, `weatherEditor`, `skySync` -- VR: `vr` - VR-specific adaptations and coordinate transformations ### Shared Utilities (`src/Utils/`) @@ -311,7 +285,6 @@ Common functionality organized by domain: - `UI.h/cpp` - ImGui utilities, input mapping, and UI helper functions - `D3D.h/cpp` - DirectX utilities and helper functions - `Game.h/cpp` - Skyrim-specific game state and object utilities -- `VRUtils.h/cpp` - VR-specific utilities and coordinate transformations - `FileSystem.h/cpp` - File I/O and path manipulation helpers - `Format.h/cpp` - String formatting and conversion utilities - `Serialize.h/cpp` - JSON serialization helpers @@ -391,7 +364,6 @@ Feature versions are automatically extracted from `.ini` files and compiled into - **Deferred Rendering Impact**: Features hook into Skyrim's rendering pipeline, adding GPU workload - **Feature Toggles**: Users can disable individual features at boot if performance is impacted (`Disable at Boot` buttons) - **A/B Testing Framework**: Built-in performance comparison system for measuring feature impact -- **VR Performance**: VR has higher performance requirements; some features may need different settings - **Tracy Profiler**: Optional build-time integration (`TRACY_SUPPORT`) for detailed performance analysis **Shader Performance Patterns**: @@ -405,7 +377,6 @@ Feature versions are automatically extracted from `.ini` files and compiled into - **In-Game Profiling**: Use Tracy integration to measure actual frame impact - **Feature Isolation**: Test features individually to identify performance bottlenecks -- **Cross-Edition Impact**: SE/AE/VR may have different performance characteristics for the same feature ### Development Performance @@ -431,7 +402,7 @@ Feature versions are automatically extracted from `.ini` files and compiled into - **Performance Concerns**: If code could impact rendering performance, suggest optimizations or user toggles - **Security Risks**: Flag potential crashes from unvalidated user input, malformed configs, or unsafe DirectX operations -- **Runtime Compatibility**: Warn when code might break SE/AE/VR compatibility or suggest `REL::RelocateMember()` patterns +- **Runtime Compatibility**: Warn when code might break SE/AE compatibility or suggest `REL::RelocateMember()` patterns - **Buffer Conflicts**: Highlight potential GPU register conflicts and recommend hlslkit buffer scanning - **Graphics Best Practices**: Suggest more idiomatic DirectX/HLSL patterns when appropriate @@ -536,7 +507,7 @@ Full details: [Developers wiki — Patch Release Process](https://github.com/com ### Testing and Validation - **Build Verification**: Always test builds after significant refactoring - this codebase has complex dependencies -- **Cross-Edition Testing**: Changes may affect SE/AE/VR differently due to engine differences +- **Cross-Edition Testing**: Changes may affect SE/AE differently due to engine differences - **Memory Management**: Pay attention to smart pointer usage and RAII patterns when modifying existing code ### Security and Input Validation @@ -567,7 +538,6 @@ Full details: [Developers wiki — Patch Release Process](https://github.com/com - **Include Dependencies**: New features often require adding includes (ShaderCache.h, imgui_stdlib.h, etc.) - **Forward Declarations**: Use forward declarations in headers when possible, full includes in .cpp files -- **VR Considerations**: VR has different rendering requirements - check VR-specific code paths when modifying graphics features - **Feature Versioning**: Feature .ini files use semantic versioning - increment appropriately when changing settings structure - **Performance Impact**: Always consider GPU workload when adding new rendering features - provide toggle options for users - **Buffer Conflicts**: Check hlslkit buffer scanning to avoid GPU register conflicts that cause rendering issues diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 0765e27928..ba8f7d5925 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -12,7 +12,7 @@ reviews: Length: 50 characters limit for title, 72 for body Style: lowercase description, no ending period Examples: - - feat(vr): add cross-eye sampling + - feat(ssgi): add temporal denoising - fix(water): resolve flowmap bug - docs: update shader documentation diff --git a/.github/configs/README.md b/.github/configs/README.md index 3dc2f95584..cd403162c8 100644 --- a/.github/configs/README.md +++ b/.github/configs/README.md @@ -5,29 +5,27 @@ This directory contains configuration files used by the CI/CD pipeline for build ## Files - `shader-validation.yaml`: Configuration for shader compilation validation using hlslkit (Skyrim SE) -- `shader-validation-vr.yaml`: VR Configuration for shader compilation validation using hlslkit (Skyrim VR) ## Generating Configuration Files These configuration files can be regenerated using the `generate-shader-configs.ps1` script in this directory. This script requires: -1. A valid Skyrim installation (SE and/or VR) +1. A valid Skyrim Special Edition installation 2. The [hlslkit](https://github.com/alandtse/hlslkit) package installed (`pip install hlslkit`) 3. Community Shaders to be run once with specific settings to generate the required log data ### Prerequisites -Before running the generation script, you must run each version of Skyrim (SE and VR) **once** with the following Community Shaders settings: +Before running the generation script, you must run Skyrim SE **once** with the following Community Shaders settings: 1. **Set Debug Log Level**: In the Community Shaders menu, set the log level to "Debug" or "Trace" 2. **Clear Disk Cache**: Clear the shader disk cache before running 3. **Enable Disk Cache**: Ensure disk cache is enabled and will be saved 4. **Run the Game**: Launch and wait for compilation to complete to generate shader compilation logs -The required log files will be created at: +The required log file will be created at: - **Skyrim SE**: `%USERPROFILE%\Documents\My Games\Skyrim Special Edition\SKSE\CommunityShaders.log` -- **Skyrim VR**: `%USERPROFILE%\Documents\My Games\Skyrim VR\SKSE\CommunityShaders.log` ### Running the Script @@ -52,11 +50,7 @@ The script will: You can also generate the files manually using hlslkit: ```bash -# For Skyrim SE hlslkit-generate --log "%USERPROFILE%\Documents\My Games\Skyrim Special Edition\SKSE\CommunityShaders.log" --output .\.github\configs\shader-validation.yaml - -# For Skyrim VR -hlslkit-generate --log "%USERPROFILE%\Documents\My Games\Skyrim VR\SKSE\CommunityShaders.log" --output .\.github\configs\shader-validation-vr.yaml ``` ## Usage in CI/CD diff --git a/.github/configs/generate-shader-configs.ps1 b/.github/configs/generate-shader-configs.ps1 index b93f1924b1..f8c78cb408 100644 --- a/.github/configs/generate-shader-configs.ps1 +++ b/.github/configs/generate-shader-configs.ps1 @@ -4,9 +4,9 @@ Generates shader validation configuration files for Community Shaders. .DESCRIPTION - This script generates shader-validation.yaml and shader-validation-vr.yaml files by analyzing - Community Shaders log files from Skyrim installations. It requires hlslkit to be installed - and both Skyrim Special Edition and/or Skyrim VR to have been run with specific settings. + This script generates shader-validation.yaml by analyzing Community Shaders log files from + Skyrim Special Edition installations. It requires hlslkit to be installed and Skyrim Special + Edition to have been run with specific settings. .PARAMETER OutputDir Directory where the generated YAML files will be saved. Defaults to current directory. @@ -88,33 +88,15 @@ function Find-SkyrimPaths { } } - # Check for Skyrim VR - $vrPath = Join-Path $myGamesPath "Skyrim VR" - if (Test-Path $vrPath) { - $paths += @{ - Name = "Skyrim VR" - Path = $vrPath - LogPath = Join-Path $vrPath "SKSE\CommunityShaders.log" - ConfigName = "shader-validation-vr.yaml" - Type = "VR" - } - } - # Check CommunityShadersOutputDir environment variable $outputDir = $env:CommunityShadersOutputDir if ($outputDir -and (Test-Path $outputDir)) { Write-Host "Found CommunityShadersOutputDir: $outputDir" -ForegroundColor Yellow - # Try to detect if this is a Skyrim installation by looking for common files $skyrimExe = Get-ChildItem -Path $outputDir -Recurse -Name "SkyrimSE.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - $skyrimVRExe = Get-ChildItem -Path $outputDir -Recurse -Name "SkyrimVR.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($skyrimExe) { Write-Host "Detected Skyrim SE installation in CommunityShadersOutputDir" -ForegroundColor Green } - if ($skyrimVRExe) { - Write-Host "Detected Skyrim VR installation in CommunityShadersOutputDir" -ForegroundColor Green - } } return $paths @@ -207,7 +189,7 @@ if ($LogFile) { $skyrimPaths = Find-SkyrimPaths if ($skyrimPaths.Count -eq 0) { - Write-Error "No Skyrim installations found. Please ensure Skyrim SE or VR is installed." + Write-Error "No Skyrim installations found. Please ensure Skyrim Special Edition is installed." exit 1 } diff --git a/.github/configs/shader-validation-vr.yaml b/.github/configs/shader-validation-vr.yaml deleted file mode 100644 index 8120904431..0000000000 --- a/.github/configs/shader-validation-vr.yaml +++ /dev/null @@ -1,23410 +0,0 @@ -common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION -common_pshader_defines: - - PSHADER -common_vshader_defines: - - VSHADER -common_cshader_defines: [] -file_common_defines: - BloodSplatter.hlsl: - PSHADER: - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - VSHADER: - - WETNESS_EFFECTS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - FLARE - - ISL - - LOD_BLENDING - CSHADER: [] - DistantTree.hlsl: - PSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SSGI - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - VSHADER: - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SSGI - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - CSHADER: [] - RunGrass.hlsl: - PSHADER: - - WETNESS_EFFECTS - - GRASS_COLLISION - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - DO_ALPHA_TEST - - ISL - - GRASS_LIGHTING - - LOD_BLENDING - VSHADER: - - WETNESS_EFFECTS - - GRASS_COLLISION - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - DO_ALPHA_TEST - - ISL - - GRASS_LIGHTING - - LOD_BLENDING - CSHADER: [] - Particle.hlsl: - PSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - VSHADER: - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - CSHADER: [] - Sky.hlsl: - PSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - VSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - CSHADER: [] - Effect.hlsl: - PSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - VSHADER: - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - CSHADER: [] - Lighting.hlsl: - PSHADER: - - TERRAIN_VARIATION - - WETNESS_EFFECTS - - SHADOWSPLITCOUNT=3 - - SCREEN_SPACE_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - CS_HAIR - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - EXTENDED_MATERIALS - - LIGHT_LIMIT_FIX - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - ISL - - LOD_BLENDING - - EXTENDED_TRANSLUCENCY - - VC - VSHADER: - - TERRAIN_VARIATION - - WETNESS_EFFECTS - - SHADOWSPLITCOUNT=3 - - SCREEN_SPACE_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - CS_HAIR - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - EXTENDED_MATERIALS - - LIGHT_LIMIT_FIX - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - ISL - - LOD_BLENDING - - EXTENDED_TRANSLUCENCY - CSHADER: [] - Water.hlsl: - PSHADER: - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - FOG - - SSS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - ISL - - WATER - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - VSHADER: - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - FOG - - SSS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - ISL - - WATER - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - CSHADER: [] - Utility.hlsl: - PSHADER: - - SHADOWSPLITCOUNT=3 - VSHADER: - - SHADOWSPLITCOUNT=3 - CSHADER: [] -warnings: - x4000:use of potentially uninitialized variable (grasscollision::getdisplacedposition): - code: X4000 - message: use of potentially uninitialized variable (GrassCollision::GetDisplacedPosition) - instances: - grasscollision/grasscollision.hlsli:52,3: - entries: - - Grass:Vertex:10000 - x4000:use of potentially uninitialized variable (terrainshadows::getterrainshadow): - code: X4000 - message: use of potentially uninitialized variable (TerrainShadows::GetTerrainShadow) - instances: - terrainshadows/terrainshadows.hlsli:27,3: - entries: - - DistantTree:Pixel:100 - - DistantTree:Pixel:10100 - - DistantTree:Pixel:10000 - - Grass:Pixel:10002 - - Effect:Pixel:514C7 - - Effect:Pixel:80114C7 - - Effect:Pixel:114C7 - - Effect:Pixel:110C7 - - Effect:Pixel:80110C7 - - Effect:Pixel:510C7 - - Effect:Pixel:80510C7 - - Effect:Pixel:8010401 - - Effect:Pixel:10401 - - Water:Pixel:1 - - Effect:Pixel:10001 - - Effect:Pixel:8010001 - - Water:Pixel:4801 - - Water:Pixel:442 - - Effect:Pixel:4010442 - - Effect:Pixel:C010442 - - Effect:Pixel:C018442 - - Effect:Pixel:4050442 - - Effect:Pixel:C038442 - - Effect:Pixel:4038442 - - Effect:Pixel:4810442 - - Effect:Pixel:4818442 - - Effect:Pixel:C838442 - - Effect:Pixel:4838442 - - Effect:Pixel:C850442 - - Effect:Pixel:850442 - - Effect:Pixel:8850442 - - Effect:Pixel:838442 - - Effect:Pixel:818442 - - Effect:Pixel:8818442 - - Effect:Pixel:8810442 - - Effect:Pixel:8038442 - - Effect:Pixel:38442 - - Effect:Pixel:18442 - - Effect:Pixel:10442 - - Effect:Pixel:8018442 - - Water:Pixel:42 - - Effect:Pixel:C018042 - - Effect:Pixel:C050042 - - Effect:Pixel:4810042 - - Effect:Pixel:C810042 - - Effect:Pixel:C818042 - - Effect:Pixel:C838042 - - Effect:Pixel:4838042 - - Effect:Pixel:4850042 - - Effect:Pixel:C850042 - - Effect:Pixel:4010042 - - Effect:Pixel:850042 - - Effect:Pixel:C010042 - - Effect:Pixel:838042 - - Effect:Pixel:8838042 - - Effect:Pixel:810042 - - Effect:Pixel:818042 - - Effect:Pixel:8818042 - - Effect:Pixel:8810042 - - Effect:Pixel:50042 - - Effect:Pixel:8050042 - - Effect:Pixel:8038042 - - Effect:Pixel:38042 - - Effect:Pixel:18042 - - Effect:Pixel:10042 - - Effect:Pixel:8018042 - - Water:Pixel:9 - - Water:Pixel:83 - - Effect:Pixel:4010443 - - Effect:Pixel:C018443 - - Effect:Pixel:C050443 - - Effect:Pixel:4038443 - - Effect:Pixel:C038443 - - Effect:Pixel:4810443 - - Effect:Pixel:C810443 - - Effect:Pixel:C818443 - - Effect:Pixel:4818443 - - Effect:Pixel:C838443 - - Effect:Pixel:4850443 - - Effect:Pixel:850443 - - Effect:Pixel:8850443 - - Effect:Pixel:8838443 - - Effect:Pixel:810443 - - Effect:Pixel:818443 - - Effect:Pixel:8050443 - - Effect:Pixel:8818443 - - Effect:Pixel:8810443 - - Effect:Pixel:38443 - - Effect:Pixel:18443 - - Effect:Pixel:8018443 - - Effect:Pixel:8010443 - - Effect:Pixel:10443 - - Effect:Pixel:8051443 - - Water:Pixel:43 - - Effect:Pixel:11443 - - Effect:Pixel:8011443 - - Effect:Pixel:4018043 - - Effect:Pixel:C050043 - - Effect:Pixel:C810043 - - Effect:Pixel:4810043 - - Effect:Pixel:4818043 - - Effect:Pixel:4838043 - - Effect:Pixel:C838043 - - Effect:Pixel:4850043 - - Effect:Pixel:C850043 - - Effect:Pixel:4010043 - - Effect:Pixel:C010043 - - Effect:Pixel:8838043 - - Effect:Pixel:818043 - - Effect:Pixel:8818043 - - Effect:Pixel:8810043 - - Effect:Pixel:810043 - - Effect:Pixel:50043 - - Effect:Pixel:8050043 - - Effect:Pixel:38043 - - Effect:Pixel:8038043 - - Effect:Pixel:18043 - - Effect:Pixel:8018043 - - Effect:Pixel:10043 - - Effect:Pixel:8051043 - - Effect:Pixel:8011043 - - Effect:Pixel:C01804A - - Effect:Pixel:401804A - - Effect:Pixel:C81804A - - Effect:Pixel:483804A - - Effect:Pixel:403804A - - Effect:Pixel:C83804A - - Effect:Pixel:C03804A - - Water:Pixel:4A - - Effect:Pixel:883804A - - Effect:Pixel:83804A - - Effect:Pixel:81804A - - Effect:Pixel:881804A - - Effect:Pixel:803804A - - Effect:Pixel:801804A - - Effect:Pixel:401844A - - Effect:Pixel:C03844A - - Effect:Pixel:481844A - - Effect:Pixel:C81844A - - Effect:Pixel:C83844A - - Water:Pixel:44A - - Effect:Pixel:883844A - - Effect:Pixel:803844A - - Effect:Pixel:881844A - - Effect:Pixel:81844A - - Effect:Pixel:1844A - - Effect:Pixel:801844A - - Effect:Pixel:C01804B - - Effect:Pixel:401804B - - Effect:Pixel:481804B - - Water:Pixel:4B - - Effect:Pixel:483804B - - Effect:Pixel:403804B - - Effect:Pixel:C83804B - - Effect:Pixel:C03804B - - Effect:Pixel:883804B - - Effect:Pixel:3804B - - Effect:Pixel:803804B - - Effect:Pixel:881804B - - Effect:Pixel:1804B - - Effect:Pixel:801804B - - Effect:Pixel:C01844B - - Effect:Pixel:401844B - - Effect:Pixel:483844B - - Effect:Pixel:481844B - - Effect:Pixel:C03844B - - Effect:Pixel:C83844B - - Effect:Pixel:83844B - - Effect:Pixel:3844B - - Effect:Pixel:883844B - - Effect:Pixel:803844B - - Effect:Pixel:801844B - - Effect:Pixel:1844B - - Effect:Pixel:4010072 - - Effect:Pixel:C010072 - - Effect:Pixel:881844B - - Effect:Pixel:C050072 - - Effect:Pixel:C018072 - - Effect:Pixel:4018072 - - Effect:Pixel:C810072 - - Effect:Pixel:4818072 - - Effect:Pixel:4810072 - - Effect:Pixel:4838072 - - Effect:Pixel:C850072 - - Effect:Pixel:C038072 - - Effect:Pixel:4038072 - - Effect:Pixel:850072 - - Effect:Pixel:8850072 - - Effect:Pixel:8838072 - - Effect:Pixel:810072 - - Effect:Pixel:8818072 - - Effect:Pixel:818072 - - Effect:Pixel:8810072 - - Effect:Pixel:50072 - - Effect:Pixel:38072 - - Effect:Pixel:8038072 - - Effect:Pixel:8010072 - - Effect:Pixel:8018072 - - Effect:Pixel:4010472 - - Effect:Pixel:C018472 - - Effect:Pixel:C010472 - - Effect:Pixel:C050472 - - Effect:Pixel:4050472 - - Effect:Pixel:C038472 - - Effect:Pixel:4038472 - - Effect:Pixel:4810472 - - Effect:Pixel:C810472 - - Effect:Pixel:4818472 - - Effect:Pixel:4838472 - - Effect:Pixel:C818472 - - Effect:Pixel:C850472 - - Effect:Pixel:850472 - - Effect:Pixel:810472 - - Effect:Pixel:818472 - - Effect:Pixel:8810472 - - Effect:Pixel:8838472 - - Effect:Pixel:838472 - - Effect:Pixel:8818472 - - Effect:Pixel:50472 - - Effect:Pixel:38472 - - Effect:Pixel:8038472 - - Effect:Pixel:8018472 - - Effect:Pixel:10472 - - Effect:Pixel:18472 - - Effect:Pixel:8010472 - - Effect:Pixel:4018073 - - Effect:Pixel:4010073 - - Effect:Pixel:C050073 - - Effect:Pixel:4050073 - - Effect:Pixel:C810073 - - Effect:Pixel:C818073 - - Effect:Pixel:4838073 - - Effect:Pixel:4818073 - - Effect:Pixel:C838073 - - Effect:Pixel:4850073 - - Effect:Pixel:4038073 - - Effect:Pixel:C038073 - - Effect:Pixel:850073 - - Effect:Pixel:8838073 - - Effect:Pixel:810073 - - Effect:Pixel:8818073 - - Effect:Pixel:8810073 - - Effect:Pixel:50073 - - Effect:Pixel:8050073 - - Effect:Pixel:38073 - - Effect:Pixel:8038073 - - Effect:Pixel:10073 - - Effect:Pixel:8018073 - - Effect:Pixel:18073 - - Effect:Pixel:4010473 - - Effect:Pixel:C018473 - - Effect:Pixel:4018473 - - Effect:Pixel:C038473 - - Effect:Pixel:4038473 - - Effect:Pixel:4050473 - - Effect:Pixel:C810473 - - Effect:Pixel:4810473 - - Effect:Pixel:C818473 - - Effect:Pixel:C838473 - - Effect:Pixel:4838473 - - Effect:Pixel:4850473 - - Effect:Pixel:C850473 - - Effect:Pixel:850473 - - Effect:Pixel:8838473 - - Effect:Pixel:838473 - - Effect:Pixel:810473 - - Effect:Pixel:818473 - - Effect:Pixel:8818473 - - Effect:Pixel:8810473 - - Effect:Pixel:50473 - - Effect:Pixel:8050473 - - Effect:Pixel:10473 - - Effect:Pixel:8018473 - - Effect:Pixel:8010473 - - Effect:Pixel:401807A - - Effect:Pixel:C01807A - - Effect:Pixel:481807A - - Effect:Pixel:C81807A - - Effect:Pixel:483807A - - Effect:Pixel:403807A - - Effect:Pixel:C03807A - - Effect:Pixel:83807A - - Effect:Pixel:883807A - - Effect:Pixel:81807A - - Effect:Pixel:881807A - - Effect:Pixel:3807A - - Effect:Pixel:803807A - - Effect:Pixel:C01847A - - Effect:Pixel:C03847A - - Effect:Pixel:481847A - - Effect:Pixel:C81847A - - Effect:Pixel:483847A - - Effect:Pixel:3847A - - Effect:Pixel:881847A - - Effect:Pixel:803847A - - Effect:Pixel:1847A - - Effect:Pixel:801847A - - Effect:Pixel:883847A - - Effect:Pixel:83847A - - Effect:Pixel:401807B - - Effect:Pixel:C81807B - - Effect:Pixel:C01807B - - Effect:Pixel:C83807B - - Effect:Pixel:403807B - - Effect:Pixel:883807B - - Effect:Pixel:83807B - - Effect:Pixel:81807B - - Effect:Pixel:881807B - - Effect:Pixel:3807B - - Effect:Pixel:803807B - - Effect:Pixel:801807B - - Effect:Pixel:401847B - - Effect:Pixel:C01847B - - Effect:Pixel:C03847B - - Effect:Pixel:481847B - - Effect:Pixel:483847B - - Effect:Pixel:C83847B - - Effect:Pixel:881847B - - Effect:Pixel:81847B - - Effect:Pixel:3847B - - Effect:Pixel:803847B - - Effect:Pixel:1847B - - Effect:Pixel:801847B - - Effect:Pixel:83847B - - Effect:Pixel:883847B - - Effect:Pixel:8010553 - - Effect:Pixel:10553 - - Effect:Pixel:50553 - - Effect:Pixel:8050553 - - Effect:Pixel:8010153 - - Effect:Pixel:50153 - - Effect:Pixel:8050153 - - Effect:Pixel:521D7 - - Effect:Pixel:80521D7 - - Effect:Pixel:121D7 - - Water:Pixel:19 - - Water:Pixel:1B - - Water:Pixel:0 - - Water:Pixel:4803 - - Water:Pixel:5803 - - Water:Pixel:3 - - Water:Pixel:11 - - Water:Pixel:211 - - Water:Pixel:203 - - Water:Pixel:213 - - Water:Pixel:411 - - Water:Pixel:413 - - Water:Pixel:601 - - Water:Pixel:611 - - Water:Pixel:603 - - Water:Pixel:613 - - Water:Pixel:401 - - Water:Pixel:202 - - Water:Pixel:82 - - Water:Pixel:1A - - Water:Pixel:9A - - Water:Pixel:9B - - Water:Pixel:283 - - Water:Pixel:683 - - Water:Pixel:21B - - Water:Pixel:21A - - Water:Pixel:282 - - Water:Pixel:61B - - Water:Pixel:61A - - Water:Pixel:12 - - Water:Pixel:29A - - Water:Pixel:69A - - Water:Pixel:8 - - Water:Pixel:A - - Water:Pixel:40 - - Water:Pixel:18 - - Water:Pixel:51 - - Water:Pixel:50 - - Water:Pixel:52 - - Water:Pixel:80 - - Water:Pixel:49 - - Water:Pixel:59 - - Water:Pixel:5B - - Water:Pixel:58 - - Water:Pixel:90 - - Water:Pixel:91 - - Water:Pixel:5A - - Water:Pixel:88 - - Water:Pixel:92 - - Water:Pixel:93 - - Water:Pixel:8A - - Water:Pixel:C0 - - Water:Pixel:C2 - - Water:Pixel:C3 - - Water:Pixel:99 - - Water:Pixel:98 - - Water:Pixel:D0 - - Water:Pixel:D1 - - Water:Pixel:D3 - - Water:Pixel:CB - - Water:Pixel:CA - - Water:Pixel:200 - - Water:Pixel:D9 - - Water:Pixel:D8 - - Water:Pixel:DB - - Water:Pixel:DA - - Water:Pixel:209 - - Water:Pixel:210 - - Water:Pixel:20A - - Water:Pixel:241 - - Water:Pixel:243 - - Water:Pixel:20B - - Water:Pixel:218 - - Water:Pixel:248 - - Water:Pixel:249 - - Water:Pixel:250 - - Water:Pixel:251 - - Water:Pixel:24A - - Water:Pixel:24B - - Water:Pixel:252 - - Water:Pixel:280 - - Water:Pixel:259 - - Water:Pixel:25B - - Water:Pixel:258 - - Water:Pixel:289 - - Water:Pixel:291 - - Water:Pixel:293 - - Water:Pixel:292 - - Water:Pixel:28B - - Water:Pixel:28A - - Water:Pixel:2C0 - - Water:Pixel:2C2 - - Water:Pixel:2C3 - - Water:Pixel:299 - - Water:Pixel:298 - - Water:Pixel:2D1 - - Water:Pixel:2D0 - - Water:Pixel:400 - - Water:Pixel:2C9 - - Water:Pixel:402 - - Water:Pixel:2CB - - Water:Pixel:2CA - - Water:Pixel:2D9 - - Water:Pixel:2D8 - - Water:Pixel:2DB - - Water:Pixel:2DA - - Water:Pixel:410 - - Water:Pixel:409 - - Water:Pixel:408 - - Water:Pixel:40A - - Water:Pixel:441 - - Water:Pixel:440 - - Water:Pixel:451 - - Water:Pixel:419 - - Water:Pixel:418 - - Water:Pixel:453 - - Water:Pixel:452 - - Water:Pixel:41A - - Water:Pixel:41B - - Water:Pixel:481 - - Water:Pixel:480 - - Water:Pixel:482 - - Water:Pixel:483 - - Water:Pixel:459 - - Water:Pixel:458 - - Water:Pixel:45B - - Water:Pixel:45A - - Water:Pixel:491 - - Water:Pixel:493 - - Water:Pixel:492 - - Water:Pixel:488 - - Water:Pixel:48B - - Water:Pixel:4C0 - - Water:Pixel:4C1 - - Water:Pixel:4C3 - - Water:Pixel:4C2 - - Water:Pixel:499 - - Water:Pixel:498 - - Water:Pixel:49B - - Water:Pixel:4D0 - - Water:Pixel:4D2 - - Water:Pixel:4C9 - - Water:Pixel:4D3 - - Water:Pixel:4C8 - - Water:Pixel:4CA - - Water:Pixel:600 - - Water:Pixel:4DB - - Water:Pixel:4D8 - - Water:Pixel:4DA - - Water:Pixel:609 - - Water:Pixel:610 - - Water:Pixel:60A - - Water:Pixel:640 - - Water:Pixel:641 - - Water:Pixel:643 - - Water:Pixel:619 - - Water:Pixel:618 - - Water:Pixel:648 - - Water:Pixel:651 - - Water:Pixel:653 - - Water:Pixel:64A - - Water:Pixel:652 - - Water:Pixel:64B - - Water:Pixel:681 - - Water:Pixel:680 - - Water:Pixel:682 - - Water:Pixel:658 - - Water:Pixel:65B - - Water:Pixel:65A - - Water:Pixel:689 - - Water:Pixel:690 - - Water:Pixel:692 - - Water:Pixel:6C0 - - Water:Pixel:6C1 - - Water:Pixel:68B - - Water:Pixel:6C3 - - Water:Pixel:6C2 - - Water:Pixel:699 - - Water:Pixel:698 - - Water:Pixel:4800 - - Water:Pixel:6D1 - - Water:Pixel:6D0 - - Water:Pixel:6C8 - - Water:Pixel:6D3 - - Water:Pixel:6D2 - - Water:Pixel:6C9 - - Water:Pixel:6CB - - Water:Pixel:4802 - - Water:Pixel:6D9 - - Water:Pixel:6DA - x4000:use of potentially uninitialized variable (shadowsampling::geteffectshadow): - code: X4000 - message: use of potentially uninitialized variable (ShadowSampling::GetEffectShadow) - instances: - common/shadowsampling.hlsli:172,3: - entries: - - Effect:Pixel:514C7 - - Effect:Pixel:80114C7 - - Effect:Pixel:114C7 - - Effect:Pixel:110C7 - - Effect:Pixel:80110C7 - - Effect:Pixel:510C7 - - Effect:Pixel:80510C7 - - Effect:Pixel:8010401 - - Effect:Pixel:10401 - - Effect:Pixel:10001 - - Effect:Pixel:8010001 - - Effect:Pixel:4010442 - - Effect:Pixel:C010442 - - Effect:Pixel:C018442 - - Effect:Pixel:4050442 - - Effect:Pixel:C038442 - - Effect:Pixel:4038442 - - Effect:Pixel:4810442 - - Effect:Pixel:4818442 - - Effect:Pixel:C838442 - - Effect:Pixel:4838442 - - Effect:Pixel:C850442 - - Effect:Pixel:850442 - - Effect:Pixel:8850442 - - Effect:Pixel:838442 - - Effect:Pixel:818442 - - Effect:Pixel:8818442 - - Effect:Pixel:8810442 - - Effect:Pixel:8038442 - - Effect:Pixel:38442 - - Effect:Pixel:18442 - - Effect:Pixel:10442 - - Effect:Pixel:8018442 - - Effect:Pixel:C018042 - - Effect:Pixel:C050042 - - Effect:Pixel:4810042 - - Effect:Pixel:C810042 - - Effect:Pixel:C818042 - - Effect:Pixel:C838042 - - Effect:Pixel:4838042 - - Effect:Pixel:4850042 - - Effect:Pixel:C850042 - - Effect:Pixel:4010042 - - Effect:Pixel:850042 - - Effect:Pixel:C010042 - - Effect:Pixel:838042 - - Effect:Pixel:8838042 - - Effect:Pixel:810042 - - Effect:Pixel:818042 - - Effect:Pixel:8818042 - - Effect:Pixel:8810042 - - Effect:Pixel:50042 - - Effect:Pixel:8050042 - - Effect:Pixel:8038042 - - Effect:Pixel:38042 - - Effect:Pixel:18042 - - Effect:Pixel:10042 - - Effect:Pixel:8018042 - - Effect:Pixel:4010443 - - Effect:Pixel:C018443 - - Effect:Pixel:C050443 - - Effect:Pixel:4038443 - - Effect:Pixel:C038443 - - Effect:Pixel:4810443 - - Effect:Pixel:C810443 - - Effect:Pixel:C818443 - - Effect:Pixel:4818443 - - Effect:Pixel:C838443 - - Effect:Pixel:4850443 - - Effect:Pixel:850443 - - Effect:Pixel:8850443 - - Effect:Pixel:8838443 - - Effect:Pixel:810443 - - Effect:Pixel:818443 - - Effect:Pixel:8050443 - - Effect:Pixel:8818443 - - Effect:Pixel:8810443 - - Effect:Pixel:38443 - - Effect:Pixel:18443 - - Effect:Pixel:8018443 - - Effect:Pixel:8010443 - - Effect:Pixel:10443 - - Effect:Pixel:8051443 - - Effect:Pixel:11443 - - Effect:Pixel:8011443 - - Effect:Pixel:4018043 - - Effect:Pixel:C050043 - - Effect:Pixel:C810043 - - Effect:Pixel:4810043 - - Effect:Pixel:4818043 - - Effect:Pixel:4838043 - - Effect:Pixel:C838043 - - Effect:Pixel:4850043 - - Effect:Pixel:C850043 - - Effect:Pixel:4010043 - - Effect:Pixel:C010043 - - Effect:Pixel:8838043 - - Effect:Pixel:818043 - - Effect:Pixel:8818043 - - Effect:Pixel:8810043 - - Effect:Pixel:810043 - - Effect:Pixel:50043 - - Effect:Pixel:8050043 - - Effect:Pixel:38043 - - Effect:Pixel:8038043 - - Effect:Pixel:18043 - - Effect:Pixel:8018043 - - Effect:Pixel:10043 - - Effect:Pixel:8051043 - - Effect:Pixel:8011043 - - Effect:Pixel:C01804A - - Effect:Pixel:401804A - - Effect:Pixel:C81804A - - Effect:Pixel:483804A - - Effect:Pixel:403804A - - Effect:Pixel:C83804A - - Effect:Pixel:C03804A - - Effect:Pixel:883804A - - Effect:Pixel:83804A - - Effect:Pixel:81804A - - Effect:Pixel:881804A - - Effect:Pixel:803804A - - Effect:Pixel:801804A - - Effect:Pixel:401844A - - Effect:Pixel:C03844A - - Effect:Pixel:481844A - - Effect:Pixel:C81844A - - Effect:Pixel:C83844A - - Effect:Pixel:883844A - - Effect:Pixel:803844A - - Effect:Pixel:881844A - - Effect:Pixel:81844A - - Effect:Pixel:1844A - - Effect:Pixel:801844A - - Effect:Pixel:C01804B - - Effect:Pixel:401804B - - Effect:Pixel:481804B - - Effect:Pixel:483804B - - Effect:Pixel:403804B - - Effect:Pixel:C83804B - - Effect:Pixel:C03804B - - Effect:Pixel:883804B - - Effect:Pixel:3804B - - Effect:Pixel:803804B - - Effect:Pixel:881804B - - Effect:Pixel:1804B - - Effect:Pixel:801804B - - Effect:Pixel:C01844B - - Effect:Pixel:401844B - - Effect:Pixel:483844B - - Effect:Pixel:481844B - - Effect:Pixel:C03844B - - Effect:Pixel:C83844B - - Effect:Pixel:83844B - - Effect:Pixel:3844B - - Effect:Pixel:883844B - - Effect:Pixel:803844B - - Effect:Pixel:801844B - - Effect:Pixel:1844B - - Effect:Pixel:4010072 - - Effect:Pixel:C010072 - - Effect:Pixel:881844B - - Effect:Pixel:C050072 - - Effect:Pixel:C018072 - - Effect:Pixel:4018072 - - Effect:Pixel:C810072 - - Effect:Pixel:4818072 - - Effect:Pixel:4810072 - - Effect:Pixel:4838072 - - Effect:Pixel:C850072 - - Effect:Pixel:C038072 - - Effect:Pixel:4038072 - - Effect:Pixel:850072 - - Effect:Pixel:8850072 - - Effect:Pixel:8838072 - - Effect:Pixel:810072 - - Effect:Pixel:8818072 - - Effect:Pixel:818072 - - Effect:Pixel:8810072 - - Effect:Pixel:50072 - - Effect:Pixel:38072 - - Effect:Pixel:8038072 - - Effect:Pixel:8010072 - - Effect:Pixel:8018072 - - Effect:Pixel:4010472 - - Effect:Pixel:C018472 - - Effect:Pixel:C010472 - - Effect:Pixel:C050472 - - Effect:Pixel:4050472 - - Effect:Pixel:C038472 - - Effect:Pixel:4038472 - - Effect:Pixel:4810472 - - Effect:Pixel:C810472 - - Effect:Pixel:4818472 - - Effect:Pixel:4838472 - - Effect:Pixel:C818472 - - Effect:Pixel:C850472 - - Effect:Pixel:850472 - - Effect:Pixel:810472 - - Effect:Pixel:818472 - - Effect:Pixel:8810472 - - Effect:Pixel:8838472 - - Effect:Pixel:838472 - - Effect:Pixel:8818472 - - Effect:Pixel:50472 - - Effect:Pixel:38472 - - Effect:Pixel:8038472 - - Effect:Pixel:8018472 - - Effect:Pixel:10472 - - Effect:Pixel:18472 - - Effect:Pixel:8010472 - - Effect:Pixel:4018073 - - Effect:Pixel:4010073 - - Effect:Pixel:C050073 - - Effect:Pixel:4050073 - - Effect:Pixel:C810073 - - Effect:Pixel:C818073 - - Effect:Pixel:4838073 - - Effect:Pixel:4818073 - - Effect:Pixel:C838073 - - Effect:Pixel:4850073 - - Effect:Pixel:4038073 - - Effect:Pixel:C038073 - - Effect:Pixel:850073 - - Effect:Pixel:8838073 - - Effect:Pixel:810073 - - Effect:Pixel:8818073 - - Effect:Pixel:8810073 - - Effect:Pixel:50073 - - Effect:Pixel:8050073 - - Effect:Pixel:38073 - - Effect:Pixel:8038073 - - Effect:Pixel:10073 - - Effect:Pixel:8018073 - - Effect:Pixel:18073 - - Effect:Pixel:4010473 - - Effect:Pixel:C018473 - - Effect:Pixel:4018473 - - Effect:Pixel:C038473 - - Effect:Pixel:4038473 - - Effect:Pixel:4050473 - - Effect:Pixel:C810473 - - Effect:Pixel:4810473 - - Effect:Pixel:C818473 - - Effect:Pixel:C838473 - - Effect:Pixel:4838473 - - Effect:Pixel:4850473 - - Effect:Pixel:C850473 - - Effect:Pixel:850473 - - Effect:Pixel:8838473 - - Effect:Pixel:838473 - - Effect:Pixel:810473 - - Effect:Pixel:818473 - - Effect:Pixel:8818473 - - Effect:Pixel:8810473 - - Effect:Pixel:50473 - - Effect:Pixel:8050473 - - Effect:Pixel:10473 - - Effect:Pixel:8018473 - - Effect:Pixel:8010473 - - Effect:Pixel:401807A - - Effect:Pixel:C01807A - - Effect:Pixel:481807A - - Effect:Pixel:C81807A - - Effect:Pixel:483807A - - Effect:Pixel:403807A - - Effect:Pixel:C03807A - - Effect:Pixel:83807A - - Effect:Pixel:883807A - - Effect:Pixel:81807A - - Effect:Pixel:881807A - - Effect:Pixel:3807A - - Effect:Pixel:803807A - - Effect:Pixel:C01847A - - Effect:Pixel:C03847A - - Effect:Pixel:481847A - - Effect:Pixel:C81847A - - Effect:Pixel:483847A - - Effect:Pixel:3847A - - Effect:Pixel:881847A - - Effect:Pixel:803847A - - Effect:Pixel:1847A - - Effect:Pixel:801847A - - Effect:Pixel:883847A - - Effect:Pixel:83847A - - Effect:Pixel:401807B - - Effect:Pixel:C81807B - - Effect:Pixel:C01807B - - Effect:Pixel:C83807B - - Effect:Pixel:403807B - - Effect:Pixel:883807B - - Effect:Pixel:83807B - - Effect:Pixel:81807B - - Effect:Pixel:881807B - - Effect:Pixel:3807B - - Effect:Pixel:803807B - - Effect:Pixel:801807B - - Effect:Pixel:401847B - - Effect:Pixel:C01847B - - Effect:Pixel:C03847B - - Effect:Pixel:481847B - - Effect:Pixel:483847B - - Effect:Pixel:C83847B - - Effect:Pixel:881847B - - Effect:Pixel:81847B - - Effect:Pixel:3847B - - Effect:Pixel:803847B - - Effect:Pixel:1847B - - Effect:Pixel:801847B - - Effect:Pixel:83847B - - Effect:Pixel:883847B - - Effect:Pixel:8010553 - - Effect:Pixel:10553 - - Effect:Pixel:50553 - - Effect:Pixel:8050553 - - Effect:Pixel:8010153 - - Effect:Pixel:50153 - - Effect:Pixel:8050153 - - Effect:Pixel:521D7 - - Effect:Pixel:80521D7 - - Effect:Pixel:121D7 - x4000:use of potentially uninitialized variable (getwaterspecularcolor): - code: X4000 - message: use of potentially uninitialized variable (GetWaterSpecularColor) - instances: - water.hlsl:828,2: - entries: - - Water:Pixel:4001 - - Water:Pixel:1 - - Water:Pixel:4801 - - Water:Pixel:442 - - Water:Pixel:42 - - Water:Pixel:4002 - - Water:Pixel:4009 - - Water:Pixel:9 - - Water:Pixel:83 - - Water:Pixel:43 - - Water:Pixel:4A - - Water:Pixel:44A - - Water:Pixel:4B - - Water:Pixel:19 - - Water:Pixel:1B - - Water:Pixel:400B - - Water:Pixel:4000 - - Water:Pixel:0 - - Water:Pixel:4803 - - Water:Pixel:5803 - - Water:Pixel:4003 - - Water:Pixel:3 - - Water:Pixel:11 - - Water:Pixel:211 - - Water:Pixel:203 - - Water:Pixel:213 - - Water:Pixel:411 - - Water:Pixel:413 - - Water:Pixel:601 - - Water:Pixel:611 - - Water:Pixel:603 - - Water:Pixel:613 - - Water:Pixel:401 - - Water:Pixel:202 - - Water:Pixel:82 - - Water:Pixel:1A - - Water:Pixel:9A - - Water:Pixel:9B - - Water:Pixel:283 - - Water:Pixel:683 - - Water:Pixel:4008 - - Water:Pixel:400A - - Water:Pixel:21B - - Water:Pixel:21A - - Water:Pixel:282 - - Water:Pixel:61B - - Water:Pixel:61A - - Water:Pixel:12 - - Water:Pixel:29A - - Water:Pixel:69A - - Water:Pixel:8 - - Water:Pixel:A - - Water:Pixel:40 - - Water:Pixel:4049 - - Water:Pixel:4048 - - Water:Pixel:18 - - Water:Pixel:51 - - Water:Pixel:50 - - Water:Pixel:52 - - Water:Pixel:80 - - Water:Pixel:49 - - Water:Pixel:59 - - Water:Pixel:5B - - Water:Pixel:58 - - Water:Pixel:90 - - Water:Pixel:91 - - Water:Pixel:5A - - Water:Pixel:88 - - Water:Pixel:92 - - Water:Pixel:93 - - Water:Pixel:8A - - Water:Pixel:C0 - - Water:Pixel:C2 - - Water:Pixel:C3 - - Water:Pixel:99 - - Water:Pixel:98 - - Water:Pixel:D0 - - Water:Pixel:D1 - - Water:Pixel:D3 - - Water:Pixel:4208 - - Water:Pixel:CB - - Water:Pixel:CA - - Water:Pixel:200 - - Water:Pixel:D9 - - Water:Pixel:D8 - - Water:Pixel:DB - - Water:Pixel:DA - - Water:Pixel:420A - - Water:Pixel:420B - - Water:Pixel:209 - - Water:Pixel:210 - - Water:Pixel:20A - - Water:Pixel:241 - - Water:Pixel:4248 - - Water:Pixel:243 - - Water:Pixel:20B - - Water:Pixel:4249 - - Water:Pixel:424A - - Water:Pixel:424B - - Water:Pixel:218 - - Water:Pixel:248 - - Water:Pixel:249 - - Water:Pixel:250 - - Water:Pixel:251 - - Water:Pixel:24A - - Water:Pixel:24B - - Water:Pixel:252 - - Water:Pixel:280 - - Water:Pixel:259 - - Water:Pixel:25B - - Water:Pixel:258 - - Water:Pixel:289 - - Water:Pixel:291 - - Water:Pixel:293 - - Water:Pixel:292 - - Water:Pixel:28B - - Water:Pixel:28A - - Water:Pixel:2C0 - - Water:Pixel:2C2 - - Water:Pixel:2C3 - - Water:Pixel:299 - - Water:Pixel:298 - - Water:Pixel:2D1 - - Water:Pixel:2D0 - - Water:Pixel:400 - - Water:Pixel:2C9 - - Water:Pixel:402 - - Water:Pixel:2CB - - Water:Pixel:2CA - - Water:Pixel:2D9 - - Water:Pixel:2D8 - - Water:Pixel:2DB - - Water:Pixel:2DA - - Water:Pixel:410 - - Water:Pixel:409 - - Water:Pixel:408 - - Water:Pixel:40A - - Water:Pixel:441 - - Water:Pixel:440 - - Water:Pixel:451 - - Water:Pixel:419 - - Water:Pixel:418 - - Water:Pixel:453 - - Water:Pixel:452 - - Water:Pixel:41A - - Water:Pixel:41B - - Water:Pixel:481 - - Water:Pixel:480 - - Water:Pixel:482 - - Water:Pixel:483 - - Water:Pixel:459 - - Water:Pixel:458 - - Water:Pixel:45B - - Water:Pixel:45A - - Water:Pixel:491 - - Water:Pixel:493 - - Water:Pixel:492 - - Water:Pixel:488 - - Water:Pixel:48B - - Water:Pixel:4C0 - - Water:Pixel:4C1 - - Water:Pixel:4C3 - - Water:Pixel:4C2 - - Water:Pixel:499 - - Water:Pixel:498 - - Water:Pixel:49B - - Water:Pixel:4D0 - - Water:Pixel:4D2 - - Water:Pixel:4C9 - - Water:Pixel:4D3 - - Water:Pixel:4C8 - - Water:Pixel:4CA - - Water:Pixel:4608 - - Water:Pixel:4609 - - Water:Pixel:600 - - Water:Pixel:460A - - Water:Pixel:4DB - - Water:Pixel:4D8 - - Water:Pixel:4DA - - Water:Pixel:460B - - Water:Pixel:609 - - Water:Pixel:610 - - Water:Pixel:60A - - Water:Pixel:640 - - Water:Pixel:641 - - Water:Pixel:4648 - - Water:Pixel:643 - - Water:Pixel:4649 - - Water:Pixel:464A - - Water:Pixel:619 - - Water:Pixel:618 - - Water:Pixel:464B - - Water:Pixel:648 - - Water:Pixel:651 - - Water:Pixel:653 - - Water:Pixel:64A - - Water:Pixel:652 - - Water:Pixel:64B - - Water:Pixel:681 - - Water:Pixel:680 - - Water:Pixel:682 - - Water:Pixel:658 - - Water:Pixel:65B - - Water:Pixel:65A - - Water:Pixel:689 - - Water:Pixel:690 - - Water:Pixel:692 - - Water:Pixel:6C0 - - Water:Pixel:6C1 - - Water:Pixel:68B - - Water:Pixel:6C3 - - Water:Pixel:6C2 - - Water:Pixel:699 - - Water:Pixel:698 - - Water:Pixel:4800 - - Water:Pixel:6D1 - - Water:Pixel:6D0 - - Water:Pixel:6C8 - - Water:Pixel:6D3 - - Water:Pixel:6D2 - - Water:Pixel:6C9 - - Water:Pixel:6CB - - Water:Pixel:4802 - - Water:Pixel:6D9 - - Water:Pixel:6DA - x4000:use of potentially uninitialized variable (shadowsampling::get2dfilteredshadow): - code: X4000 - message: use of potentially uninitialized variable (ShadowSampling::Get2DFilteredShadow) - instances: - common/shadowsampling.hlsli:140,3: - entries: - - Water:Pixel:1 - - Water:Pixel:4801 - - Water:Pixel:442 - - Water:Pixel:42 - - Water:Pixel:9 - - Water:Pixel:83 - - Water:Pixel:43 - - Water:Pixel:4A - - Water:Pixel:44A - - Water:Pixel:4B - - Water:Pixel:19 - - Water:Pixel:1B - - Water:Pixel:0 - - Water:Pixel:4803 - - Water:Pixel:5803 - - Water:Pixel:3 - - Water:Pixel:11 - - Water:Pixel:211 - - Water:Pixel:203 - - Water:Pixel:213 - - Water:Pixel:411 - - Water:Pixel:413 - - Water:Pixel:601 - - Water:Pixel:611 - - Water:Pixel:603 - - Water:Pixel:613 - - Water:Pixel:401 - - Water:Pixel:202 - - Water:Pixel:82 - - Water:Pixel:1A - - Water:Pixel:9A - - Water:Pixel:9B - - Water:Pixel:283 - - Water:Pixel:683 - - Water:Pixel:21B - - Water:Pixel:21A - - Water:Pixel:282 - - Water:Pixel:61B - - Water:Pixel:61A - - Water:Pixel:12 - - Water:Pixel:29A - - Water:Pixel:69A - - Water:Pixel:8 - - Water:Pixel:A - - Water:Pixel:40 - - Water:Pixel:18 - - Water:Pixel:51 - - Water:Pixel:50 - - Water:Pixel:52 - - Water:Pixel:80 - - Water:Pixel:49 - - Water:Pixel:59 - - Water:Pixel:5B - - Water:Pixel:58 - - Water:Pixel:90 - - Water:Pixel:91 - - Water:Pixel:5A - - Water:Pixel:88 - - Water:Pixel:92 - - Water:Pixel:93 - - Water:Pixel:8A - - Water:Pixel:C0 - - Water:Pixel:C2 - - Water:Pixel:C3 - - Water:Pixel:99 - - Water:Pixel:98 - - Water:Pixel:D0 - - Water:Pixel:D1 - - Water:Pixel:D3 - - Water:Pixel:CB - - Water:Pixel:CA - - Water:Pixel:200 - - Water:Pixel:D9 - - Water:Pixel:D8 - - Water:Pixel:DB - - Water:Pixel:DA - - Water:Pixel:209 - - Water:Pixel:210 - - Water:Pixel:20A - - Water:Pixel:241 - - Water:Pixel:243 - - Water:Pixel:20B - - Water:Pixel:218 - - Water:Pixel:248 - - Water:Pixel:249 - - Water:Pixel:250 - - Water:Pixel:251 - - Water:Pixel:24A - - Water:Pixel:24B - - Water:Pixel:252 - - Water:Pixel:280 - - Water:Pixel:259 - - Water:Pixel:25B - - Water:Pixel:258 - - Water:Pixel:289 - - Water:Pixel:291 - - Water:Pixel:293 - - Water:Pixel:292 - - Water:Pixel:28B - - Water:Pixel:28A - - Water:Pixel:2C0 - - Water:Pixel:2C2 - - Water:Pixel:2C3 - - Water:Pixel:299 - - Water:Pixel:298 - - Water:Pixel:2D1 - - Water:Pixel:2D0 - - Water:Pixel:400 - - Water:Pixel:2C9 - - Water:Pixel:402 - - Water:Pixel:2CB - - Water:Pixel:2CA - - Water:Pixel:2D9 - - Water:Pixel:2D8 - - Water:Pixel:2DB - - Water:Pixel:2DA - - Water:Pixel:410 - - Water:Pixel:409 - - Water:Pixel:408 - - Water:Pixel:40A - - Water:Pixel:441 - - Water:Pixel:440 - - Water:Pixel:451 - - Water:Pixel:419 - - Water:Pixel:418 - - Water:Pixel:453 - - Water:Pixel:452 - - Water:Pixel:41A - - Water:Pixel:41B - - Water:Pixel:481 - - Water:Pixel:480 - - Water:Pixel:482 - - Water:Pixel:483 - - Water:Pixel:459 - - Water:Pixel:458 - - Water:Pixel:45B - - Water:Pixel:45A - - Water:Pixel:491 - - Water:Pixel:493 - - Water:Pixel:492 - - Water:Pixel:488 - - Water:Pixel:48B - - Water:Pixel:4C0 - - Water:Pixel:4C1 - - Water:Pixel:4C3 - - Water:Pixel:4C2 - - Water:Pixel:499 - - Water:Pixel:498 - - Water:Pixel:49B - - Water:Pixel:4D0 - - Water:Pixel:4D2 - - Water:Pixel:4C9 - - Water:Pixel:4D3 - - Water:Pixel:4C8 - - Water:Pixel:4CA - - Water:Pixel:600 - - Water:Pixel:4DB - - Water:Pixel:4D8 - - Water:Pixel:4DA - - Water:Pixel:609 - - Water:Pixel:610 - - Water:Pixel:60A - - Water:Pixel:640 - - Water:Pixel:641 - - Water:Pixel:643 - - Water:Pixel:619 - - Water:Pixel:618 - - Water:Pixel:648 - - Water:Pixel:651 - - Water:Pixel:653 - - Water:Pixel:64A - - Water:Pixel:652 - - Water:Pixel:64B - - Water:Pixel:681 - - Water:Pixel:680 - - Water:Pixel:682 - - Water:Pixel:658 - - Water:Pixel:65B - - Water:Pixel:65A - - Water:Pixel:689 - - Water:Pixel:690 - - Water:Pixel:692 - - Water:Pixel:6C0 - - Water:Pixel:6C1 - - Water:Pixel:68B - - Water:Pixel:6C3 - - Water:Pixel:6C2 - - Water:Pixel:699 - - Water:Pixel:698 - - Water:Pixel:4800 - - Water:Pixel:6D1 - - Water:Pixel:6D0 - - Water:Pixel:6C8 - - Water:Pixel:6D3 - - Water:Pixel:6D2 - - Water:Pixel:6C9 - - Water:Pixel:6CB - - Water:Pixel:4802 - - Water:Pixel:6D9 - - Water:Pixel:6DA - x4000:use of potentially uninitialized variable (shadowsampling::getwatershadow): - code: X4000 - message: use of potentially uninitialized variable (ShadowSampling::GetWaterShadow) - instances: - common/shadowsampling.hlsli:194,3: - entries: - - Water:Pixel:1 - - Water:Pixel:4801 - - Water:Pixel:442 - - Water:Pixel:42 - - Water:Pixel:9 - - Water:Pixel:83 - - Water:Pixel:43 - - Water:Pixel:4A - - Water:Pixel:44A - - Water:Pixel:4B - - Water:Pixel:19 - - Water:Pixel:1B - - Water:Pixel:0 - - Water:Pixel:4803 - - Water:Pixel:5803 - - Water:Pixel:3 - - Water:Pixel:11 - - Water:Pixel:211 - - Water:Pixel:203 - - Water:Pixel:213 - - Water:Pixel:411 - - Water:Pixel:413 - - Water:Pixel:601 - - Water:Pixel:611 - - Water:Pixel:603 - - Water:Pixel:613 - - Water:Pixel:401 - - Water:Pixel:202 - - Water:Pixel:82 - - Water:Pixel:1A - - Water:Pixel:9A - - Water:Pixel:9B - - Water:Pixel:283 - - Water:Pixel:683 - - Water:Pixel:21B - - Water:Pixel:21A - - Water:Pixel:282 - - Water:Pixel:61B - - Water:Pixel:61A - - Water:Pixel:12 - - Water:Pixel:29A - - Water:Pixel:69A - - Water:Pixel:8 - - Water:Pixel:A - - Water:Pixel:40 - - Water:Pixel:18 - - Water:Pixel:51 - - Water:Pixel:50 - - Water:Pixel:52 - - Water:Pixel:80 - - Water:Pixel:49 - - Water:Pixel:59 - - Water:Pixel:5B - - Water:Pixel:58 - - Water:Pixel:90 - - Water:Pixel:91 - - Water:Pixel:5A - - Water:Pixel:88 - - Water:Pixel:92 - - Water:Pixel:93 - - Water:Pixel:8A - - Water:Pixel:C0 - - Water:Pixel:C2 - - Water:Pixel:C3 - - Water:Pixel:99 - - Water:Pixel:98 - - Water:Pixel:D0 - - Water:Pixel:D1 - - Water:Pixel:D3 - - Water:Pixel:CB - - Water:Pixel:CA - - Water:Pixel:200 - - Water:Pixel:D9 - - Water:Pixel:D8 - - Water:Pixel:DB - - Water:Pixel:DA - - Water:Pixel:209 - - Water:Pixel:210 - - Water:Pixel:20A - - Water:Pixel:241 - - Water:Pixel:243 - - Water:Pixel:20B - - Water:Pixel:218 - - Water:Pixel:248 - - Water:Pixel:249 - - Water:Pixel:250 - - Water:Pixel:251 - - Water:Pixel:24A - - Water:Pixel:24B - - Water:Pixel:252 - - Water:Pixel:280 - - Water:Pixel:259 - - Water:Pixel:25B - - Water:Pixel:258 - - Water:Pixel:289 - - Water:Pixel:291 - - Water:Pixel:293 - - Water:Pixel:292 - - Water:Pixel:28B - - Water:Pixel:28A - - Water:Pixel:2C0 - - Water:Pixel:2C2 - - Water:Pixel:2C3 - - Water:Pixel:299 - - Water:Pixel:298 - - Water:Pixel:2D1 - - Water:Pixel:2D0 - - Water:Pixel:400 - - Water:Pixel:2C9 - - Water:Pixel:402 - - Water:Pixel:2CB - - Water:Pixel:2CA - - Water:Pixel:2D9 - - Water:Pixel:2D8 - - Water:Pixel:2DB - - Water:Pixel:2DA - - Water:Pixel:410 - - Water:Pixel:409 - - Water:Pixel:408 - - Water:Pixel:40A - - Water:Pixel:441 - - Water:Pixel:440 - - Water:Pixel:451 - - Water:Pixel:419 - - Water:Pixel:418 - - Water:Pixel:453 - - Water:Pixel:452 - - Water:Pixel:41A - - Water:Pixel:41B - - Water:Pixel:481 - - Water:Pixel:480 - - Water:Pixel:482 - - Water:Pixel:483 - - Water:Pixel:459 - - Water:Pixel:458 - - Water:Pixel:45B - - Water:Pixel:45A - - Water:Pixel:491 - - Water:Pixel:493 - - Water:Pixel:492 - - Water:Pixel:488 - - Water:Pixel:48B - - Water:Pixel:4C0 - - Water:Pixel:4C1 - - Water:Pixel:4C3 - - Water:Pixel:4C2 - - Water:Pixel:499 - - Water:Pixel:498 - - Water:Pixel:49B - - Water:Pixel:4D0 - - Water:Pixel:4D2 - - Water:Pixel:4C9 - - Water:Pixel:4D3 - - Water:Pixel:4C8 - - Water:Pixel:4CA - - Water:Pixel:600 - - Water:Pixel:4DB - - Water:Pixel:4D8 - - Water:Pixel:4DA - - Water:Pixel:609 - - Water:Pixel:610 - - Water:Pixel:60A - - Water:Pixel:640 - - Water:Pixel:641 - - Water:Pixel:643 - - Water:Pixel:619 - - Water:Pixel:618 - - Water:Pixel:648 - - Water:Pixel:651 - - Water:Pixel:653 - - Water:Pixel:64A - - Water:Pixel:652 - - Water:Pixel:64B - - Water:Pixel:681 - - Water:Pixel:680 - - Water:Pixel:682 - - Water:Pixel:658 - - Water:Pixel:65B - - Water:Pixel:65A - - Water:Pixel:689 - - Water:Pixel:690 - - Water:Pixel:692 - - Water:Pixel:6C0 - - Water:Pixel:6C1 - - Water:Pixel:68B - - Water:Pixel:6C3 - - Water:Pixel:6C2 - - Water:Pixel:699 - - Water:Pixel:698 - - Water:Pixel:4800 - - Water:Pixel:6D1 - - Water:Pixel:6D0 - - Water:Pixel:6C8 - - Water:Pixel:6D3 - - Water:Pixel:6D2 - - Water:Pixel:6C9 - - Water:Pixel:6CB - - Water:Pixel:4802 - - Water:Pixel:6D9 - - Water:Pixel:6DA - x3595:gradient instruction used in a loop with varying iteration; partial derivatives may have undefined value: - code: X3595 - message: gradient instruction used in a loop with varying iteration; partial derivatives - may have undefined value - instances: - lighting.hlsl:697,24-90: - entries: - - Lighting:Pixel:E008001 - - Lighting:Pixel:E009001 - - Lighting:Pixel:E108001 - - Lighting:Pixel:E109001 - - Lighting:Pixel:E009011 - - Lighting:Pixel:E008011 - - Lighting:Pixel:E109011 - - Lighting:Pixel:E008201 - - Lighting:Pixel:E108201 - - Lighting:Pixel:E009201 - - Lighting:Pixel:E108211 - - Lighting:Pixel:E109211 - - Lighting:Pixel:E009211 - - Lighting:Pixel:E008211 - lighting.hlsl:698,24-90: - entries: - - Lighting:Pixel:E008001 - - Lighting:Pixel:E009001 - - Lighting:Pixel:E108001 - - Lighting:Pixel:E109001 - - Lighting:Pixel:E009011 - - Lighting:Pixel:E008011 - - Lighting:Pixel:E109011 - - Lighting:Pixel:E008201 - - Lighting:Pixel:E108201 - - Lighting:Pixel:E009201 - - Lighting:Pixel:E108211 - - Lighting:Pixel:E109211 - - Lighting:Pixel:E009211 - - Lighting:Pixel:E008211 - lighting.hlsl:699,24-90: - entries: - - Lighting:Pixel:E008001 - - Lighting:Pixel:E009001 - - Lighting:Pixel:E108001 - - Lighting:Pixel:E109001 - - Lighting:Pixel:E009011 - - Lighting:Pixel:E008011 - - Lighting:Pixel:E109011 - - Lighting:Pixel:E008201 - - Lighting:Pixel:E108201 - - Lighting:Pixel:E009201 - - Lighting:Pixel:E108211 - - Lighting:Pixel:E109211 - - Lighting:Pixel:E009211 - - Lighting:Pixel:E008211 - x3557:loop only executes for 1 iteration(s), forcing loop to unroll: - code: X3557 - message: loop only executes for 1 iteration(s), forcing loop to unroll - instances: - water.hlsl:1050,2-73: - entries: - - Water:Pixel:801 - - Water:Pixel:843 - - Water:Pixel:803 - - Water:Pixel:800 - - Water:Pixel:802 - - Water:Pixel:840 - - Water:Pixel:841 -errors: {} -shaders: - - file: BloodSplatter.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - entries: - - entry: BloodSplatter:Pixel:0 - defines: - - SPLATTER - - entry: BloodSplatter:Pixel:1 - defines: - - FLARE - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - WETNESS_EFFECTS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - FLARE - - ISL - - LOD_BLENDING - entries: - - entry: BloodSplatter:Vertex:1 - defines: [] - - file: DistantTree.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SSGI - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: DistantTree:Pixel:10101 - defines: - - RENDER_DEPTH - - DO_ALPHA_TEST - - DEFERRED - - entry: DistantTree:Pixel:10000 - defines: - - DO_ALPHA_TEST - - entry: DistantTree:Pixel:101 - defines: - - RENDER_DEPTH - - DEFERRED - - entry: DistantTree:Pixel:10001 - defines: - - RENDER_DEPTH - - DO_ALPHA_TEST - - entry: DistantTree:Pixel:100 - defines: - - DEFERRED - - entry: DistantTree:Pixel:1 - defines: - - RENDER_DEPTH - - entry: DistantTree:Pixel:10100 - defines: - - DO_ALPHA_TEST - - DEFERRED - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SSGI - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - entries: - - entry: DistantTree:Vertex:0 - defines: [] - - entry: DistantTree:Vertex:10000 - defines: - - DO_ALPHA_TEST - - entry: DistantTree:Vertex:1 - defines: - - RENDER_DEPTH - - entry: DistantTree:Vertex:10001 - defines: - - RENDER_DEPTH - - DO_ALPHA_TEST - - file: RunGrass.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - WETNESS_EFFECTS - - GRASS_COLLISION - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - DO_ALPHA_TEST - - ISL - - GRASS_LIGHTING - - LOD_BLENDING - entries: - - entry: Grass:Pixel:10002 - defines: [] - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - WETNESS_EFFECTS - - GRASS_COLLISION - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - DO_ALPHA_TEST - - ISL - - GRASS_LIGHTING - - LOD_BLENDING - entries: - - entry: Grass:Vertex:10000 - defines: [] - - file: Particle.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: Particle:Pixel:3 - defines: - - GRAYSCALE_TO_COLOR - - GRAYSCALE_TO_ALPHA - - entry: Particle:Pixel:4 - defines: - - ENVCUBE - - SNOW - - entry: Particle:Pixel:5 - defines: - - ENVCUBE - - RAIN - - entry: Particle:Pixel:1 - defines: - - GRAYSCALE_TO_COLOR - - entry: Particle:Pixel:2 - defines: - - GRAYSCALE_TO_ALPHA - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - entries: - - entry: Particle:Vertex:3 - defines: - - GRAYSCALE_TO_COLOR - - GRAYSCALE_TO_ALPHA - - entry: Particle:Vertex:4 - defines: - - ENVCUBE - - SNOW - - entry: Particle:Vertex:5 - defines: - - ENVCUBE - - RAIN - - entry: Particle:Vertex:0 - defines: [] - - entry: Particle:Vertex:1 - defines: - - GRAYSCALE_TO_COLOR - - entry: Particle:Vertex:2 - defines: - - GRAYSCALE_TO_ALPHA - - file: Sky.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: Sky:Pixel:3 - defines: - - HORIZFADE - - entry: Sky:Pixel:103 - defines: - - HORIZFADE - - DEFERRED - - entry: Sky:Pixel:104 - defines: - - TEX - - CLOUDS - - DEFERRED - - entry: Sky:Pixel:105 - defines: - - TEX - - CLOUDS - - TEXLERP - - DEFERRED - - entry: Sky:Pixel:5 - defines: - - TEX - - CLOUDS - - TEXLERP - - entry: Sky:Pixel:106 - defines: - - TEX - - CLOUDS - - TEXFADE - - DEFERRED - - entry: Sky:Pixel:6 - defines: - - TEX - - CLOUDS - - TEXFADE - - entry: Sky:Pixel:107 - defines: - - TEX - - DEFERRED - - entry: Sky:Pixel:8 - defines: - - DITHER - - entry: Sky:Pixel:108 - defines: - - DITHER - - DEFERRED - - entry: Sky:Pixel:0 - defines: - - OCCLUSION - - entry: Sky:Pixel:100 - defines: - - OCCLUSION - - DEFERRED - - entry: Sky:Pixel:1 - defines: - - TEX - - DITHER - - entry: Sky:Pixel:101 - defines: - - TEX - - DITHER - - DEFERRED - - entry: Sky:Pixel:102 - defines: - - TEX - - MOONMASK - - DEFERRED - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: Sky:Vertex:4 - defines: - - TEX - - CLOUDS - - entry: Sky:Vertex:5 - defines: - - TEX - - CLOUDS - - TEXLERP - - entry: Sky:Vertex:6 - defines: - - TEX - - CLOUDS - - TEXFADE - - entry: Sky:Vertex:7 - defines: - - TEX - - entry: Sky:Vertex:8 - defines: - - DITHER - - entry: Sky:Vertex:0 - defines: - - OCCLUSION - - entry: Sky:Vertex:1 - defines: - - TEX - - DITHER - - entry: Sky:Vertex:2 - defines: - - TEX - - MOONMASK - - file: Effect.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - SKYLIGHTING - - IBL - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: Effect:Pixel:414C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - SOFT - - entry: Effect:Pixel:80014C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - DEFERRED - - entry: Effect:Pixel:14C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - entry: Effect:Pixel:514C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - SOFT - - entry: Effect:Pixel:114C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - entry: Effect:Pixel:80114C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:80010C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - DEFERRED - - entry: Effect:Pixel:10C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - entry: Effect:Pixel:80110C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:110C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - LIGHTING - - entry: Effect:Pixel:80410C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:410C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - SOFT - - entry: Effect:Pixel:510C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - LIGHTING - - SOFT - - entry: Effect:Pixel:80510C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:400 - defines: - - ADDBLEND - - entry: Effect:Pixel:8000400 - defines: - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:C000000 - defines: - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4000000 - defines: - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000800 - defines: - - MULTBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:0 - defines: [] - - entry: Effect:Pixel:8000000 - defines: - - DEFERRED - - entry: Effect:Pixel:8040401 - defines: - - VC - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:40401 - defines: - - VC - - ADDBLEND - - SOFT - - entry: Effect:Pixel:8001401 - defines: - - VC - - ADDBLEND - - PARTICLES - - DEFERRED - - entry: Effect:Pixel:8000401 - defines: - - VC - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:401 - defines: - - VC - - ADDBLEND - - entry: Effect:Pixel:8010401 - defines: - - VC - - ADDBLEND - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10401 - defines: - - VC - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8041401 - defines: - - VC - - ADDBLEND - - PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:41401 - defines: - - VC - - ADDBLEND - - PARTICLES - - SOFT - - entry: Effect:Pixel:8000001 - defines: - - VC - - DEFERRED - - entry: Effect:Pixel:1 - defines: - - VC - - entry: Effect:Pixel:10001 - defines: - - VC - - LIGHTING - - entry: Effect:Pixel:8010001 - defines: - - VC - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:40001 - defines: - - VC - - SOFT - - entry: Effect:Pixel:8040001 - defines: - - VC - - SOFT - - DEFERRED - - entry: Effect:Pixel:4000001 - defines: - - VC - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000001 - defines: - - VC - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4040001 - defines: - - VC - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040001 - defines: - - VC - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C000801 - defines: - - VC - - MULTBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:801 - defines: - - VC - - MULTBLEND - - entry: Effect:Pixel:8000801 - defines: - - VC - - MULTBLEND - - DEFERRED - - entry: Effect:Pixel:C010442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4010442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C018442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4050442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C038442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4800442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4810442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4818442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C850442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4008442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:8800442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8850442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:838442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:818442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:8810442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:40442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Pixel:8040442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:38442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:28442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:8028442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:18442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:8008442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - entry: Effect:Pixel:8000442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:4000842 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000842 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C018042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C028042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4040042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C050042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C808042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C818042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C850042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4850042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4010042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C010042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4008042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C008042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4000042 - defines: - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000042 - defines: - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:800042 - defines: - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - entry: Effect:Pixel:8800042 - defines: - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:818042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:400042 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - entry: Effect:Pixel:8400042 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - DEFERRED - - entry: Effect:Pixel:50042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:40042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - entry: Effect:Pixel:38042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:8028042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:18042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - entry: Effect:Pixel:8042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:42 - defines: - - TEXCOORD - - TEXTURE - - entry: Effect:Pixel:8000042 - defines: - - TEXCOORD - - TEXTURE - - DEFERRED - - entry: Effect:Pixel:20013 - defines: - - VC - - TEXCOORD - - NORMALS - - PROJECTED_UV - - entry: Effect:Pixel:8002083 - defines: - - VC - - TEXCOORD - - INDEXED_TEXTURE - - STRIP_PARTICLES - - DEFERRED - - entry: Effect:Pixel:4010443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C018443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C028443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4040443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C050443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C038443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C818443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4818443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4828443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4850443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4008443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4000443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Pixel:8800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8850443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:840443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:8838443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:8050443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:818443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:40443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Pixel:8040443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:38443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:28443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:8028443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:18443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8010443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8008443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - entry: Effect:Pixel:8051443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:1443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - entry: Effect:Pixel:41443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - SOFT - - entry: Effect:Pixel:11443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - entry: Effect:Pixel:8011443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:4018043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C028043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4040043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C050043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4818043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4828043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C850043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4850043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4010043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C010043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4008043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C008043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4000043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - entry: Effect:Pixel:8800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:840043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8838043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:818043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:400043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - entry: Effect:Pixel:8400043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - DEFERRED - - entry: Effect:Pixel:50043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:8040043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - DEFERRED - - entry: Effect:Pixel:38043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:28043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:18043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - entry: Effect:Pixel:8043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:43 - defines: - - VC - - TEXCOORD - - TEXTURE - - entry: Effect:Pixel:8000043 - defines: - - VC - - TEXCOORD - - TEXTURE - - DEFERRED - - entry: Effect:Pixel:41043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - SOFT - - entry: Effect:Pixel:8051043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:8011043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8001043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - DEFERRED - - entry: Effect:Pixel:4843 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND - - BLOOD - - entry: Effect:Pixel:8004843 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND - - BLOOD - - DEFERRED - - entry: Effect:Pixel:8000843 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND - - DEFERRED - - entry: Effect:Pixel:C01804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C80804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:480804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C81804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C82804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:483804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:403804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C00804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:83804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:82804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:81804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:881804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:803804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:2804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:801804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:800804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:401844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C80844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C81844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:481844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C00844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:883844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:82844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:882844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:803844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:81844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:881844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:880844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:2844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:1844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:801844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:800844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:C01804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:480804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:481804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:483804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:403804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C00804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:883804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:882804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:881804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:880804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:3804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:2804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:1804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:801804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:800804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:C01844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C80844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:480844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:481844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:483844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C00844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:83844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:82844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:3844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:881844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:880844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:2844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:1844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:801844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:800844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:C010072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4010072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C018072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4018072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4028072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C050072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4800072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4818072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C828072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C850072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C038072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4008072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4000072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:800072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - entry: Effect:Pixel:8800072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8850072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:840072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8838072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:818072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:50072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Pixel:40072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Pixel:8040072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - DEFERRED - - entry: Effect:Pixel:38072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:28072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:8028072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:8018072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8010072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:8008072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:72 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Pixel:8000072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - DEFERRED - - entry: Effect:Pixel:C010472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4010472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C018472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C028472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4040472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C050472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4050472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C038472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4808472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C828472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4828472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4838472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C850472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C008472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C000472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Pixel:8800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:840472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:838472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8838472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:50472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Pixel:8040472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:38472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:28472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:18472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8010472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - entry: Effect:Pixel:8000472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:4010073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4018073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C028073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4028073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4040073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C050073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4050073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4800073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C810073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C818073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4818073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C828073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4828073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4850073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C038073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4008073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C008073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4000073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C000073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:8800073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:840073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:8838073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:8818073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:808073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:8808073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:50073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:40073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Pixel:8040073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - DEFERRED - - entry: Effect:Pixel:38073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:8038073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:28073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:8028073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:18073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:8018073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - entry: Effect:Pixel:8073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:8008073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:73 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Pixel:8000073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - DEFERRED - - entry: Effect:Pixel:4010473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C018473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4018473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C038473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4038473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C040473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4040473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4050473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C808473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4808473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C810473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4810473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C818473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4828473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C838473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4838473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C840473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4840473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C850473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4850473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:4008473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C008473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:4000473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Pixel:8800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:850473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Pixel:8840473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:838473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8838473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:828473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:8828473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:818473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8818473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:810473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:8810473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:50473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:40473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Pixel:8040473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:28473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:8028473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:8018473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:10473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8010473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:8008473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:8000473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:C01807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:480807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C81807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:481807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:483807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:403807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:83807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:82807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:882807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:81807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:881807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:880807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:3807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:2807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:800807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:C01847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C03847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C02847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C80847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:480847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C81847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:481847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:483847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:882847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:881847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:880847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:3847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:802847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:1847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:801847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:800847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - DEFERRED - - entry: Effect:Pixel:83847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:C01807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C02807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C80807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C81807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C82807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:403807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C00807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:83807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:82807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:81807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:881807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:880807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:3807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:2807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Pixel:802807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:801807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Pixel:C01847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:401847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C03847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:C02847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:402847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C80847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:481847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C82847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:482847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C83847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:483847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:400847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:C00847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:882847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:81847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Pixel:881847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Pixel:880847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:3847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Pixel:803847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Effect:Pixel:1847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Pixel:801847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Pixel:83847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Pixel:883847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - DEFERRED - - entry: Effect:Pixel:80024D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - STRIP_PARTICLES - - DEFERRED - - entry: Effect:Pixel:24D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - STRIP_PARTICLES - - entry: Effect:Pixel:424D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - entry: Effect:Pixel:80424D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:80420D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - STRIP_PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:20D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - STRIP_PARTICLES - - entry: Effect:Pixel:80020D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - STRIP_PARTICLES - - DEFERRED - - entry: Effect:Pixel:8000511 - defines: - - VC - - NORMALS - - FALLOFF - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:8000111 - defines: - - VC - - NORMALS - - FALLOFF - - DEFERRED - - entry: Effect:Pixel:111 - defines: - - VC - - NORMALS - - FALLOFF - - entry: Effect:Pixel:40111 - defines: - - VC - - NORMALS - - FALLOFF - - SOFT - - entry: Effect:Pixel:8040111 - defines: - - VC - - NORMALS - - FALLOFF - - SOFT - - DEFERRED - - entry: Effect:Pixel:8000513 - defines: - - VC - - TEXCOORD - - NORMALS - - FALLOFF - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:8000552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:40552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - entry: Effect:Pixel:8040552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:8040152 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - SOFT - - DEFERRED - - entry: Effect:Pixel:40152 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - SOFT - - entry: Effect:Pixel:152 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - entry: Effect:Pixel:C000553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - MOTIONVECTORS_NORMALS - - DEFERRED - - entry: Effect:Pixel:10553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - entry: Effect:Pixel:8010553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - entry: Effect:Pixel:8000553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - DEFERRED - - entry: Effect:Pixel:50553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:8040553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:42553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - entry: Effect:Pixel:8042553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:4000153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MOTIONVECTORS_NORMALS - - entry: Effect:Pixel:8010153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - LIGHTING - - DEFERRED - - entry: Effect:Pixel:8000153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - DEFERRED - - entry: Effect:Pixel:50153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - LIGHTING - - SOFT - - entry: Effect:Pixel:8050153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:8000953 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MULTBLEND - - DEFERRED - - entry: Effect:Pixel:40953 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MULTBLEND - - SOFT - - entry: Effect:Pixel:8040953 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MULTBLEND - - SOFT - - DEFERRED - - entry: Effect:Pixel:80025D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - DEFERRED - - entry: Effect:Pixel:25D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - entry: Effect:Pixel:425D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - entry: Effect:Pixel:80425D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - DEFERRED - - entry: Effect:Pixel:80521D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - LIGHTING - - SOFT - - DEFERRED - - entry: Effect:Pixel:521D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - LIGHTING - - SOFT - - entry: Effect:Pixel:421D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - SOFT - - entry: Effect:Pixel:80021D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - DEFERRED - - entry: Effect:Pixel:121D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - LIGHTING - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - LIGHT_LIMIT_FIX - - WETNESS_EFFECTS - - TERRAIN_SHADOWS - - CLOUD_SHADOWS - - DYNAMIC_CUBEMAPS - - WATER_EFFECTS - - SSS - - IBL - - SKYLIGHTING - - ISL - - LOD_BLENDING - - SCREEN_SPACE_SHADOWS - entries: - - entry: Effect:Vertex:114C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - entry: Effect:Vertex:14C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - entry: Effect:Vertex:414C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - SOFT - - entry: Effect:Vertex:514C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - PARTICLES - - LIGHTING - - SOFT - - entry: Effect:Vertex:110C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - LIGHTING - - entry: Effect:Vertex:10C7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - TEXTURE - - INDEXED_TEXTURE - - PARTICLES - - entry: Effect:Vertex:408 - defines: - - SKINNED - - ADDBLEND - - entry: Effect:Vertex:400 - defines: - - ADDBLEND - - entry: Effect:Vertex:10401 - defines: - - VC - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:401 - defines: - - VC - - ADDBLEND - - entry: Effect:Vertex:1401 - defines: - - VC - - ADDBLEND - - PARTICLES - - entry: Effect:Vertex:41401 - defines: - - VC - - ADDBLEND - - PARTICLES - - SOFT - - entry: Effect:Vertex:4040001 - defines: - - VC - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:1 - defines: - - VC - - entry: Effect:Vertex:40001 - defines: - - VC - - SOFT - - entry: Effect:Vertex:10001 - defines: - - VC - - LIGHTING - - entry: Effect:Vertex:4000001 - defines: - - VC - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:801 - defines: - - VC - - MULTBLEND - - entry: Effect:Vertex:4000801 - defines: - - VC - - MULTBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4008442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:800442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:50442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:40442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:8442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:28442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:818442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:828442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:838442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850442 - defines: - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4842 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND - - BLOOD - - entry: Effect:Vertex:4808042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800042 - defines: - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4008042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4850042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:808042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:800042 - defines: - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:400042 - defines: - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - entry: Effect:Vertex:38042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:50042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Vertex:40042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - entry: Effect:Vertex:8042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:10042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:42 - defines: - - TEXCOORD - - TEXTURE - - entry: Effect:Vertex:18042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:28042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:818042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:828042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:838042 - defines: - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840042 - defines: - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850042 - defines: - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4000042 - defines: - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:9 - defines: - - VC - - SKINNED - - entry: Effect:Vertex:20013 - defines: - - VC - - TEXCOORD - - NORMALS - - PROJECTED_UV - - entry: Effect:Vertex:2083 - defines: - - VC - - TEXCOORD - - INDEXED_TEXTURE - - STRIP_PARTICLES - - entry: Effect:Vertex:4800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:4028443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4850443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:808443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:800443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:50443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:40443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:38443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:8443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:1443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - PARTICLES - - entry: Effect:Vertex:443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:10443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:18443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:838443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:828443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:1000443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - SKY_OBJECT - - entry: Effect:Vertex:4000443 - defines: - - VC - - TEXCOORD - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4008043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4850043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:808043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:800043 - defines: - - VC - - TEXCOORD - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:400043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND_DECAL - - entry: Effect:Vertex:38043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:41043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - SOFT - - entry: Effect:Vertex:40043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SOFT - - entry: Effect:Vertex:8043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:1043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - entry: Effect:Vertex:10043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:43 - defines: - - VC - - TEXCOORD - - TEXTURE - - entry: Effect:Vertex:11043 - defines: - - VC - - TEXCOORD - - TEXTURE - - PARTICLES - - LIGHTING - - entry: Effect:Vertex:18043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:828043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:838043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:850043 - defines: - - VC - - TEXCOORD - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:1000043 - defines: - - VC - - TEXCOORD - - TEXTURE - - SKY_OBJECT - - entry: Effect:Vertex:4000043 - defines: - - VC - - TEXCOORD - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4843 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND - - BLOOD - - entry: Effect:Vertex:843 - defines: - - VC - - TEXCOORD - - TEXTURE - - MULTBLEND - - entry: Effect:Vertex:480804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:400804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:484004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:3804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:5004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Vertex:4004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - SOFT - - entry: Effect:Vertex:1804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:81804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:82804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83804A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:85004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400004A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:403844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:5044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:3844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:44A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:1044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:1844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:2844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:81844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:83844A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:84044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:85044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400044A - defines: - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:400804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:80804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:40004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MULTBLEND_DECAL - - entry: Effect:Vertex:3804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:4004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - SOFT - - entry: Effect:Vertex:4B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - entry: Effect:Vertex:804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:1004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:1804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:2804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:82804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83804B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:85004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400004B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:403844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:400844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:484044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:4044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:3844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:1044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:1844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:2844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:81844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:82844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83844B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:84044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:85044B - defines: - - VC - - TEXCOORD - - SKINNED - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4808072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4008072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4850072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:800072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:50072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Vertex:40072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Vertex:38072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:8072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:72 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Vertex:10072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:28072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:818072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:828072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:838072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4000072 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:4008472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:800472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:50472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:40472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:38472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:10472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:18472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:818472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:828472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4000472 - defines: - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4050073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4028073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4838073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4810073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:808073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:40073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Vertex:38073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:8073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:73 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Vertex:10073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:28073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:818073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:838073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4000073 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4040473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4038473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4018473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4010473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:810473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:4008473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4808473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4840473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4828473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:4818473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:808473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:800473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:50473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:40473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:28473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:38473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:10473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:818473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:838473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:840473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:850473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:4000473 - defines: - - VC - - TEXCOORD - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:400807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:80807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:5007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Vertex:4007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Vertex:3807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - entry: Effect:Vertex:7A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Vertex:1007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:1807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:2807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:81807A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:84007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:85007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400007A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:401047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:400847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:484047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:5047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:4047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:2847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:3847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:47A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:1047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:1847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:81847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:82847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83847A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:84047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:85047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400047A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:405007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:401007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:484007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - ALPHA_TEST - - entry: Effect:Vertex:80007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ALPHA_TEST - - entry: Effect:Vertex:5007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - entry: Effect:Vertex:4007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - entry: Effect:Vertex:3807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:7B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - entry: Effect:Vertex:1007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - entry: Effect:Vertex:1807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - entry: Effect:Vertex:2807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:82807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83807B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:84007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:85007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400007B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:404047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:403847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:402847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:81047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:401047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:485047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:484047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:483847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:482847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:481847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:480847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - ALPHA_TEST - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:80047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - ALPHA_TEST - - entry: Effect:Vertex:5047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:4047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - SOFT - - entry: Effect:Vertex:2847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - entry: Effect:Vertex:3847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - entry: Effect:Vertex:847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - entry: Effect:Vertex:47B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - entry: Effect:Vertex:1047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:81847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - ALPHA_TEST - - entry: Effect:Vertex:82847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:83847B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MEMBRANE - - LIGHTING - - PROJECTED_UV - - ALPHA_TEST - - entry: Effect:Vertex:85047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - LIGHTING - - SOFT - - ALPHA_TEST - - entry: Effect:Vertex:400047B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - BINORMAL_TANGENT - - TEXTURE - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:424D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - entry: Effect:Vertex:420D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - STRIP_PARTICLES - - SOFT - - entry: Effect:Vertex:20D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - STRIP_PARTICLES - - entry: Effect:Vertex:511 - defines: - - VC - - NORMALS - - FALLOFF - - ADDBLEND - - entry: Effect:Vertex:40111 - defines: - - VC - - NORMALS - - FALLOFF - - SOFT - - entry: Effect:Vertex:111 - defines: - - VC - - NORMALS - - FALLOFF - - entry: Effect:Vertex:1000513 - defines: - - VC - - TEXCOORD - - NORMALS - - FALLOFF - - ADDBLEND - - SKY_OBJECT - - entry: Effect:Vertex:119 - defines: - - VC - - SKINNED - - NORMALS - - FALLOFF - - entry: Effect:Vertex:40119 - defines: - - VC - - SKINNED - - NORMALS - - FALLOFF - - SOFT - - entry: Effect:Vertex:4000552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:40552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - entry: Effect:Vertex:552 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - entry: Effect:Vertex:40152 - defines: - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - SOFT - - entry: Effect:Vertex:4000553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:10553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:42553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - SOFT - - entry: Effect:Vertex:553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - entry: Effect:Vertex:50553 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:40153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - SOFT - - entry: Effect:Vertex:153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - entry: Effect:Vertex:10153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - LIGHTING - - entry: Effect:Vertex:1000153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - SKY_OBJECT - - entry: Effect:Vertex:4000153 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MOTIONVECTORS_NORMALS - - entry: Effect:Vertex:40953 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MULTBLEND - - SOFT - - entry: Effect:Vertex:953 - defines: - - VC - - TEXCOORD - - NORMALS - - TEXTURE - - FALLOFF - - MULTBLEND - - entry: Effect:Vertex:55A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - entry: Effect:Vertex:4055A - defines: - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - entry: Effect:Vertex:100055B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SKY_OBJECT - - entry: Effect:Vertex:1055B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - entry: Effect:Vertex:4055B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - SOFT - - entry: Effect:Vertex:55B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - entry: Effect:Vertex:5055B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - ADDBLEND - - LIGHTING - - SOFT - - entry: Effect:Vertex:15B - defines: - - VC - - TEXCOORD - - SKINNED - - NORMALS - - TEXTURE - - FALLOFF - - entry: Effect:Vertex:25D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - ADDBLEND - - STRIP_PARTICLES - - entry: Effect:Vertex:121D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - LIGHTING - - entry: Effect:Vertex:521D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - LIGHTING - - SOFT - - entry: Effect:Vertex:421D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - SOFT - - entry: Effect:Vertex:21D7 - defines: - - VC - - TEXCOORD - - TEXCOORD_INDEX - - NORMALS - - TEXTURE - - INDEXED_TEXTURE - - FALLOFF - - STRIP_PARTICLES - - file: Lighting.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - TERRAIN_VARIATION - - WETNESS_EFFECTS - - SHADOWSPLITCOUNT=3 - - SCREEN_SPACE_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - CS_HAIR - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - EXTENDED_MATERIALS - - LIGHT_LIMIT_FIX - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - ISL - - LOD_BLENDING - - EXTENDED_TRANSLUCENCY - - VC - entries: - - entry: Lighting:Pixel:5000401 - defines: - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100401 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:5100401 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:100401 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:4000401 - defines: - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:C000401 - defines: - - SOFT_LIGHTING - - TREE_ANIM - - entry: Lighting:Pixel:1100401 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:C100401 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:C108401 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:10C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:100C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:101C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1100C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - entry: Lighting:Pixel:1C01 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - entry: Lighting:Pixel:9001 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - entry: Lighting:Pixel:10001 - defines: - - ANISO_LIGHTING - - entry: Lighting:Pixel:110001 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1008001 - defines: - - PROJECTED_UV - - ENVMAP - - entry: Lighting:Pixel:1010001 - defines: - - ANISO_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1000001 - defines: - - ENVMAP - - entry: Lighting:Pixel:1108001 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1 - defines: [] - - entry: Lighting:Pixel:1001 - defines: - - BACK_LIGHTING - - entry: Lighting:Pixel:13000001 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - entry: Lighting:Pixel:F048001 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTSHD - - entry: Lighting:Pixel:F008001 - defines: - - PROJECTED_UV - - LODOBJECTSHD - - entry: Lighting:Pixel:F140001 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTSHD - - entry: Lighting:Pixel:F108001 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTSHD - - entry: Lighting:Pixel:F100001 - defines: - - DO_ALPHA_TEST - - LODOBJECTSHD - - entry: Lighting:Pixel:F040001 - defines: - - WORLD_MAP - - LODOBJECTSHD - - entry: Lighting:Pixel:F000001 - defines: - - LODOBJECTSHD - - entry: Lighting:Pixel:E109001 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:E108001 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:E009001 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:E008001 - defines: - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:D008001 - defines: - - PROJECTED_UV - - LODOBJECTS - - entry: Lighting:Pixel:D148001 - defines: - - PROJECTED_UV - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - entry: Lighting:Pixel:D140001 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - entry: Lighting:Pixel:D108001 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTS - - entry: Lighting:Pixel:D040001 - defines: - - WORLD_MAP - - LODOBJECTS - - entry: Lighting:Pixel:D000001 - defines: - - LODOBJECTS - - entry: Lighting:Pixel:C100001 - defines: - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:B101001 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000001 - defines: - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001001 - defines: - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:100001 - defines: - - DO_ALPHA_TEST - - entry: Lighting:Pixel:8000001 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - entry: Lighting:Pixel:6110001 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6100001 - defines: - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6010001 - defines: - - ANISO_LIGHTING - - HAIR - - entry: Lighting:Pixel:3100001 - defines: - - DO_ALPHA_TEST - - PARALLAX - - entry: Lighting:Pixel:101001 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:8001 - defines: - - PROJECTED_UV - - entry: Lighting:Pixel:1100001 - defines: - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1110001 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1101001 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:11801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:111801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:101801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000801 - defines: - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1011801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1111801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1100801 - defines: - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - entry: Lighting:Pixel:B101801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B100801 - defines: - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000801 - defines: - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:1101801 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:8009 - defines: - - PROJECTED_UV - - TRUE_PBR - - entry: Lighting:Pixel:9 - defines: - - TRUE_PBR - - entry: Lighting:Pixel:100009 - defines: - - DO_ALPHA_TEST - - TRUE_PBR - - entry: Lighting:Pixel:F048009 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTSHD - - TRUE_PBR - - entry: Lighting:Pixel:18009 - defines: - - PROJECTED_UV - - ANISO_LIGHTING - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:F040009 - defines: - - WORLD_MAP - - LODOBJECTSHD - - TRUE_PBR - - entry: Lighting:Pixel:10009 - defines: - - ANISO_LIGHTING - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:118009 - defines: - - PROJECTED_UV - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:F140009 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTSHD - - TRUE_PBR - - entry: Lighting:Pixel:110009 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:D008009 - defines: - - PROJECTED_UV - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D000009 - defines: - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D100009 - defines: - - DO_ALPHA_TEST - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D048009 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D040009 - defines: - - WORLD_MAP - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D140009 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:D148009 - defines: - - PROJECTED_UV - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - TRUE_PBR - - entry: Lighting:Pixel:F000009 - defines: - - LODOBJECTSHD - - TRUE_PBR - - entry: Lighting:Pixel:F108009 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTSHD - - TRUE_PBR - - entry: Lighting:Pixel:C100009 - defines: - - DO_ALPHA_TEST - - TREE_ANIM - - TRUE_PBR - - entry: Lighting:Pixel:C110009 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:C010009 - defines: - - ANISO_LIGHTING - - TREE_ANIM - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:8000009 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - TRUE_PBR - - entry: Lighting:Pixel:8010009 - defines: - - ANISO_LIGHTING - - MULTI_TEXTURE - - LANDSCAPE - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:13000009 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - TRUE_PBR - - entry: Lighting:Pixel:1000813 - defines: - - SKINNED - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:813 - defines: - - SKINNED - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:B001813 - defines: - - SKINNED - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B101813 - defines: - - SKINNED - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B100813 - defines: - - SKINNED - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000813 - defines: - - SKINNED - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100813 - defines: - - SKINNED - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:10013 - defines: - - SKINNED - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:110013 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000013 - defines: - - SKINNED - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100013 - defines: - - SKINNED - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:13 - defines: - - SKINNED - - DEFERRED - - entry: Lighting:Pixel:1013 - defines: - - SKINNED - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10000013 - defines: - - SKINNED - - EYE - - DEFERRED - - entry: Lighting:Pixel:C100013 - defines: - - SKINNED - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000013 - defines: - - SKINNED - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B101013 - defines: - - SKINNED - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B001013 - defines: - - SKINNED - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000013 - defines: - - SKINNED - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:6118013 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6110013 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6100013 - defines: - - SKINNED - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6010013 - defines: - - SKINNED - - ANISO_LIGHTING - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6000013 - defines: - - SKINNED - - HAIR - - DEFERRED - - entry: Lighting:Pixel:5100013 - defines: - - SKINNED - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4000013 - defines: - - SKINNED - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100013 - defines: - - SKINNED - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1110013 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1101013 - defines: - - SKINNED - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:108019 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:100019 - defines: - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:19 - defines: - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:8019 - defines: - - PROJECTED_UV - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:118019 - defines: - - PROJECTED_UV - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:F140019 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:110019 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:F040019 - defines: - - WORLD_MAP - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:F048019 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D100019 - defines: - - DO_ALPHA_TEST - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D108019 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D000019 - defines: - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D008019 - defines: - - PROJECTED_UV - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D148019 - defines: - - PROJECTED_UV - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:D040019 - defines: - - WORLD_MAP - - LODOBJECTS - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:F100019 - defines: - - DO_ALPHA_TEST - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:F108019 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:C110019 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:F000019 - defines: - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:F008019 - defines: - - PROJECTED_UV - - LODOBJECTSHD - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:C000019 - defines: - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:8000019 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:8010019 - defines: - - ANISO_LIGHTING - - MULTI_TEXTURE - - LANDSCAPE - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:13010019 - defines: - - ANISO_LIGHTING - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:1B - defines: - - SKINNED - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:1001B - defines: - - SKINNED - - ANISO_LIGHTING - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:10001B - defines: - - SKINNED - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:11001B - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:C00001B - defines: - - SKINNED - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:C01001B - defines: - - SKINNED - - ANISO_LIGHTING - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:C10001B - defines: - - SKINNED - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - entry: Lighting:Pixel:C11001B - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:B - defines: - - SKINNED - - TRUE_PBR - - entry: Lighting:Pixel:1000B - defines: - - SKINNED - - ANISO_LIGHTING - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:10000B - defines: - - SKINNED - - DO_ALPHA_TEST - - TRUE_PBR - - entry: Lighting:Pixel:11000B - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:C00000B - defines: - - SKINNED - - TREE_ANIM - - TRUE_PBR - - entry: Lighting:Pixel:C11000B - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - TRUE_PBR - - GLINT - - entry: Lighting:Pixel:1000803 - defines: - - SKINNED - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:803 - defines: - - SKINNED - - RIM_LIGHTING - - entry: Lighting:Pixel:B001803 - defines: - - SKINNED - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B101803 - defines: - - SKINNED - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B100803 - defines: - - SKINNED - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000803 - defines: - - SKINNED - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:100803 - defines: - - SKINNED - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1100803 - defines: - - SKINNED - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:110003 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000003 - defines: - - SKINNED - - ENVMAP - - entry: Lighting:Pixel:1001003 - defines: - - SKINNED - - BACK_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:3 - defines: - - SKINNED - - entry: Lighting:Pixel:10100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - EYE - - entry: Lighting:Pixel:10000003 - defines: - - SKINNED - - EYE - - entry: Lighting:Pixel:C100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:B100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B101003 - defines: - - SKINNED - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001003 - defines: - - SKINNED - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:6118003 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - entry: Lighting:Pixel:6110003 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6108003 - defines: - - SKINNED - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - entry: Lighting:Pixel:6100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6010003 - defines: - - SKINNED - - ANISO_LIGHTING - - HAIR - - entry: Lighting:Pixel:6000003 - defines: - - SKINNED - - HAIR - - entry: Lighting:Pixel:4100003 - defines: - - SKINNED - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000003 - defines: - - SKINNED - - FACEGEN - - entry: Lighting:Pixel:101003 - defines: - - SKINNED - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1110003 - defines: - - SKINNED - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:100005 - defines: - - MODELSPACENORMALS - - DO_ALPHA_TEST - - entry: Lighting:Pixel:5 - defines: - - MODELSPACENORMALS - - entry: Lighting:Pixel:12040005 - defines: - - MODELSPACENORMALS - - WORLD_MAP - - LODLANDSCAPE - - LODLANDNOISE - - entry: Lighting:Pixel:9040005 - defines: - - MODELSPACENORMALS - - WORLD_MAP - - LODLANDSCAPE - - entry: Lighting:Pixel:9000005 - defines: - - MODELSPACENORMALS - - LODLANDSCAPE - - entry: Lighting:Pixel:100007 - defines: - - SKINNED - - MODELSPACENORMALS - - DO_ALPHA_TEST - - entry: Lighting:Pixel:7 - defines: - - SKINNED - - MODELSPACENORMALS - - entry: Lighting:Pixel:109011 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1008011 - defines: - - PROJECTED_UV - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1001011 - defines: - - BACK_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1010011 - defines: - - ANISO_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1000011 - defines: - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:11 - defines: - - DEFERRED - - entry: Lighting:Pixel:1011 - defines: - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:13000011 - defines: - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - DEFERRED - - entry: Lighting:Pixel:F048011 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTSHD - - DEFERRED - - entry: Lighting:Pixel:F008011 - defines: - - PROJECTED_UV - - LODOBJECTSHD - - DEFERRED - - entry: Lighting:Pixel:F108011 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTSHD - - DEFERRED - - entry: Lighting:Pixel:F100011 - defines: - - DO_ALPHA_TEST - - LODOBJECTSHD - - DEFERRED - - entry: Lighting:Pixel:F040011 - defines: - - WORLD_MAP - - LODOBJECTSHD - - DEFERRED - - entry: Lighting:Pixel:E109011 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:E009011 - defines: - - BACK_LIGHTING - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:E008011 - defines: - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:D048011 - defines: - - PROJECTED_UV - - WORLD_MAP - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:D008011 - defines: - - PROJECTED_UV - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:D140011 - defines: - - WORLD_MAP - - DO_ALPHA_TEST - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:D108011 - defines: - - PROJECTED_UV - - DO_ALPHA_TEST - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:D040011 - defines: - - WORLD_MAP - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:D000011 - defines: - - LODOBJECTS - - DEFERRED - - entry: Lighting:Pixel:C100011 - defines: - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000011 - defines: - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B101011 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000011 - defines: - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B001011 - defines: - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100011 - defines: - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:6110011 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6100011 - defines: - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6010011 - defines: - - ANISO_LIGHTING - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6000011 - defines: - - HAIR - - DEFERRED - - entry: Lighting:Pixel:3100011 - defines: - - DO_ALPHA_TEST - - PARALLAX - - DEFERRED - - entry: Lighting:Pixel:3000011 - defines: - - PARALLAX - - DEFERRED - - entry: Lighting:Pixel:101011 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:8011 - defines: - - PROJECTED_UV - - DEFERRED - - entry: Lighting:Pixel:1100011 - defines: - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1110011 - defines: - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1101011 - defines: - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:11811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:101811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000811 - defines: - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1001811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1011811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:811 - defines: - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:1811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:B101811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B100811 - defines: - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B001811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100811 - defines: - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1101811 - defines: - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:15 - defines: - - MODELSPACENORMALS - - DEFERRED - - entry: Lighting:Pixel:100015 - defines: - - MODELSPACENORMALS - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:12040015 - defines: - - MODELSPACENORMALS - - WORLD_MAP - - LODLANDSCAPE - - LODLANDNOISE - - DEFERRED - - entry: Lighting:Pixel:12000015 - defines: - - MODELSPACENORMALS - - LODLANDSCAPE - - LODLANDNOISE - - DEFERRED - - entry: Lighting:Pixel:9040015 - defines: - - MODELSPACENORMALS - - WORLD_MAP - - LODLANDSCAPE - - DEFERRED - - entry: Lighting:Pixel:9000015 - defines: - - MODELSPACENORMALS - - LODLANDSCAPE - - DEFERRED - - entry: Lighting:Pixel:100017 - defines: - - SKINNED - - MODELSPACENORMALS - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:17 - defines: - - SKINNED - - MODELSPACENORMALS - - DEFERRED - - entry: Lighting:Pixel:11A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:111A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000A01 - defines: - - SPECULAR - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1001A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1111A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:A01 - defines: - - SPECULAR - - RIM_LIGHTING - - entry: Lighting:Pixel:1A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - entry: Lighting:Pixel:B101A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B100A01 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000A01 - defines: - - SPECULAR - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:100A01 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1101A01 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1100A01 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:9201 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - entry: Lighting:Pixel:109201 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - entry: Lighting:Pixel:110201 - defines: - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1008201 - defines: - - SPECULAR - - PROJECTED_UV - - ENVMAP - - entry: Lighting:Pixel:1001201 - defines: - - SPECULAR - - BACK_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1000201 - defines: - - SPECULAR - - ENVMAP - - entry: Lighting:Pixel:1108201 - defines: - - SPECULAR - - PROJECTED_UV - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:201 - defines: - - SPECULAR - - entry: Lighting:Pixel:13000201 - defines: - - SPECULAR - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - entry: Lighting:Pixel:E108201 - defines: - - SPECULAR - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:E009201 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:E008201 - defines: - - SPECULAR - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - entry: Lighting:Pixel:C100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:C000201 - defines: - - SPECULAR - - TREE_ANIM - - entry: Lighting:Pixel:B100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B101201 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000201 - defines: - - SPECULAR - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001201 - defines: - - SPECULAR - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:8000201 - defines: - - SPECULAR - - MULTI_TEXTURE - - LANDSCAPE - - entry: Lighting:Pixel:6110201 - defines: - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6010201 - defines: - - SPECULAR - - ANISO_LIGHTING - - HAIR - - entry: Lighting:Pixel:3100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - PARALLAX - - entry: Lighting:Pixel:3000201 - defines: - - SPECULAR - - PARALLAX - - entry: Lighting:Pixel:101201 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - entry: Lighting:Pixel:108201 - defines: - - SPECULAR - - PROJECTED_UV - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1100201 - defines: - - SPECULAR - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:1101201 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:11A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:111A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:101A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000A11 - defines: - - SPECULAR - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1001A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1011A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - ANISO_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:A11 - defines: - - SPECULAR - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:1A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:B101A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B100A11 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B001A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000A11 - defines: - - SPECULAR - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100A11 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1101A11 - defines: - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100A11 - defines: - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:9211 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Lighting:Pixel:10211 - defines: - - SPECULAR - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:110211 - defines: - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1008211 - defines: - - SPECULAR - - PROJECTED_UV - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1010211 - defines: - - SPECULAR - - ANISO_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1000211 - defines: - - SPECULAR - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:211 - defines: - - SPECULAR - - DEFERRED - - entry: Lighting:Pixel:1211 - defines: - - SPECULAR - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:13000211 - defines: - - SPECULAR - - MULTI_TEXTURE - - LANDSCAPE - - LOD_LAND_BLEND - - DEFERRED - - entry: Lighting:Pixel:E109211 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:E108211 - defines: - - SPECULAR - - PROJECTED_UV - - DO_ALPHA_TEST - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:E009211 - defines: - - SPECULAR - - BACK_LIGHTING - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:E008211 - defines: - - SPECULAR - - PROJECTED_UV - - MULTI_INDEX - - SPARKLE - - DEFERRED - - entry: Lighting:Pixel:C100211 - defines: - - SPECULAR - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000211 - defines: - - SPECULAR - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B101211 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000211 - defines: - - SPECULAR - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B001211 - defines: - - SPECULAR - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:8000211 - defines: - - SPECULAR - - MULTI_TEXTURE - - LANDSCAPE - - DEFERRED - - entry: Lighting:Pixel:6110211 - defines: - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6100211 - defines: - - SPECULAR - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6010211 - defines: - - SPECULAR - - ANISO_LIGHTING - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6000211 - defines: - - SPECULAR - - HAIR - - DEFERRED - - entry: Lighting:Pixel:3100211 - defines: - - SPECULAR - - DO_ALPHA_TEST - - PARALLAX - - DEFERRED - - entry: Lighting:Pixel:3000211 - defines: - - SPECULAR - - PARALLAX - - DEFERRED - - entry: Lighting:Pixel:101211 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:108211 - defines: - - SPECULAR - - PROJECTED_UV - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:8211 - defines: - - SPECULAR - - PROJECTED_UV - - DEFERRED - - entry: Lighting:Pixel:1100211 - defines: - - SPECULAR - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1101211 - defines: - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1000A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - entry: Lighting:Pixel:B001A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B101A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:100A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1100A03 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:10203 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - entry: Lighting:Pixel:110203 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000203 - defines: - - SKINNED - - SPECULAR - - ENVMAP - - entry: Lighting:Pixel:1100203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:203 - defines: - - SKINNED - - SPECULAR - - entry: Lighting:Pixel:1203 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - entry: Lighting:Pixel:10000203 - defines: - - SKINNED - - SPECULAR - - EYE - - entry: Lighting:Pixel:C000203 - defines: - - SKINNED - - SPECULAR - - TREE_ANIM - - entry: Lighting:Pixel:B100203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B101203 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B001203 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:B000203 - defines: - - SKINNED - - SPECULAR - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:6110203 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6108203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - entry: Lighting:Pixel:6100203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6010203 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - HAIR - - entry: Lighting:Pixel:6000203 - defines: - - SKINNED - - SPECULAR - - HAIR - - entry: Lighting:Pixel:4100203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000203 - defines: - - SKINNED - - SPECULAR - - FACEGEN - - entry: Lighting:Pixel:101203 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:100203 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1101203 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:B001A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B101A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B100A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1100A13 - defines: - - SKINNED - - SPECULAR - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1000213 - defines: - - SKINNED - - SPECULAR - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1001213 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:213 - defines: - - SKINNED - - SPECULAR - - DEFERRED - - entry: Lighting:Pixel:1213 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - EYE - - DEFERRED - - entry: Lighting:Pixel:10000213 - defines: - - SKINNED - - SPECULAR - - EYE - - DEFERRED - - entry: Lighting:Pixel:C100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000213 - defines: - - SKINNED - - SPECULAR - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B001213 - defines: - - SKINNED - - SPECULAR - - BACK_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000213 - defines: - - SKINNED - - SPECULAR - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:6118213 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6110213 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6108213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - DEPTH_WRITE_DECALS - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6010213 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - HAIR - - DEFERRED - - entry: Lighting:Pixel:6000213 - defines: - - SKINNED - - SPECULAR - - HAIR - - DEFERRED - - entry: Lighting:Pixel:5100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000213 - defines: - - SKINNED - - SPECULAR - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:4000213 - defines: - - SKINNED - - SPECULAR - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100213 - defines: - - SKINNED - - SPECULAR - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1110213 - defines: - - SKINNED - - SPECULAR - - ANISO_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100205 - defines: - - MODELSPACENORMALS - - SPECULAR - - DO_ALPHA_TEST - - entry: Lighting:Pixel:205 - defines: - - MODELSPACENORMALS - - SPECULAR - - entry: Lighting:Pixel:100215 - defines: - - MODELSPACENORMALS - - SPECULAR - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:215 - defines: - - MODELSPACENORMALS - - SPECULAR - - DEFERRED - - entry: Lighting:Pixel:100207 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - DO_ALPHA_TEST - - entry: Lighting:Pixel:18411 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10411 - defines: - - SOFT_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:118411 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000411 - defines: - - SOFT_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:411 - defines: - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:C100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C108411 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000411 - defines: - - SOFT_LIGHTING - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C008411 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000411 - defines: - - SOFT_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:5100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000411 - defines: - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100411 - defines: - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:10C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:100C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:101C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:110C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1100C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:C11 - defines: - - SOFT_LIGHTING - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10403 - defines: - - SKINNED - - SOFT_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:110403 - defines: - - SKINNED - - SOFT_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:403 - defines: - - SKINNED - - SOFT_LIGHTING - - entry: Lighting:Pixel:10100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - EYE - - entry: Lighting:Pixel:C100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:C000403 - defines: - - SKINNED - - SOFT_LIGHTING - - TREE_ANIM - - entry: Lighting:Pixel:6100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6000403 - defines: - - SKINNED - - SOFT_LIGHTING - - HAIR - - entry: Lighting:Pixel:5100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:5000403 - defines: - - SKINNED - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100403 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000403 - defines: - - SKINNED - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:100C03 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000C03 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:1100C03 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:C03 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - entry: Lighting:Pixel:1C03 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - entry: Lighting:Pixel:100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000413 - defines: - - SKINNED - - SOFT_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:413 - defines: - - SKINNED - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - EYE - - DEFERRED - - entry: Lighting:Pixel:10000413 - defines: - - SKINNED - - SOFT_LIGHTING - - EYE - - DEFERRED - - entry: Lighting:Pixel:C100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:6000413 - defines: - - SKINNED - - SOFT_LIGHTING - - HAIR - - DEFERRED - - entry: Lighting:Pixel:5100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100413 - defines: - - SKINNED - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100C13 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:101C13 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000C13 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100C13 - defines: - - SKINNED - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - entry: Lighting:Pixel:5100405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:5000405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000405 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:415 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:5000415 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100415 - defines: - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100407 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:5100407 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:5000407 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100407 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000407 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:100417 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:417 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:5100417 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000417 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100417 - defines: - - SKINNED - - MODELSPACENORMALS - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:18601 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - entry: Lighting:Pixel:10601 - defines: - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:118601 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:110601 - defines: - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000601 - defines: - - SPECULAR - - SOFT_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:601 - defines: - - SPECULAR - - SOFT_LIGHTING - - entry: Lighting:Pixel:C108601 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:C100601 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - entry: Lighting:Pixel:C008601 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - TREE_ANIM - - entry: Lighting:Pixel:B100601 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Pixel:100601 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:10E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:100E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:101E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:110E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1100E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - entry: Lighting:Pixel:E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - entry: Lighting:Pixel:1E01 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - entry: Lighting:Pixel:8611 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - DEFERRED - - entry: Lighting:Pixel:18611 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:108611 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:110611 - defines: - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000611 - defines: - - SPECULAR - - SOFT_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100611 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:611 - defines: - - SPECULAR - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:C100611 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C108611 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - DO_ALPHA_TEST - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C000611 - defines: - - SPECULAR - - SOFT_LIGHTING - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:C008611 - defines: - - SPECULAR - - SOFT_LIGHTING - - PROJECTED_UV - - TREE_ANIM - - DEFERRED - - entry: Lighting:Pixel:B100611 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:B000611 - defines: - - SPECULAR - - SOFT_LIGHTING - - MULTI_LAYER_PARALLAX - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:100611 - defines: - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:10E11 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:101E11 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000E11 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100E11 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:E11 - defines: - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - entry: Lighting:Pixel:100603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - entry: Lighting:Pixel:10100603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - EYE - - entry: Lighting:Pixel:10000603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - EYE - - entry: Lighting:Pixel:6100603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - HAIR - - entry: Lighting:Pixel:6000603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - HAIR - - entry: Lighting:Pixel:5100603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:5000603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100603 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:1000E03 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - entry: Lighting:Pixel:E03 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - entry: Lighting:Pixel:101E03 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:10613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:100613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:110613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:1000613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:10000613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - EYE - - DEFERRED - - entry: Lighting:Pixel:6100613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - HAIR - - DEFERRED - - entry: Lighting:Pixel:5100613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4000613 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:1000E13 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:1100E13 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DO_ALPHA_TEST - - ENVMAP - - DEFERRED - - entry: Lighting:Pixel:E13 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:1E13 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:101E13 - defines: - - SKINNED - - SPECULAR - - SOFT_LIGHTING - - RIM_LIGHTING - - BACK_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:605 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - entry: Lighting:Pixel:5100605 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:5000605 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100605 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000605 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:100615 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:5100615 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000615 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4000615 - defines: - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:100607 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:607 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - entry: Lighting:Pixel:5100607 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - entry: Lighting:Pixel:4100607 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - entry: Lighting:Pixel:4000607 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN - - entry: Lighting:Pixel:100617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - DEFERRED - - entry: Lighting:Pixel:617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DEFERRED - - entry: Lighting:Pixel:5100617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:5000617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN_RGB_TINT - - DEFERRED - - entry: Lighting:Pixel:4100617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - DO_ALPHA_TEST - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:4000617 - defines: - - SKINNED - - MODELSPACENORMALS - - SPECULAR - - SOFT_LIGHTING - - FACEGEN - - DEFERRED - - entry: Lighting:Pixel:18401 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - entry: Lighting:Pixel:118401 - defines: - - SOFT_LIGHTING - - PROJECTED_UV - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:110401 - defines: - - SOFT_LIGHTING - - ANISO_LIGHTING - - DO_ALPHA_TEST - - entry: Lighting:Pixel:1000401 - defines: - - SOFT_LIGHTING - - ENVMAP - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - TERRAIN_VARIATION - - WETNESS_EFFECTS - - SHADOWSPLITCOUNT=3 - - SCREEN_SPACE_SHADOWS - - DYNAMIC_CUBEMAPS - - SSS - - CS_HAIR - - WATER_EFFECTS - - SSGI - - IBL - - SKYLIGHTING - - EXTENDED_MATERIALS - - LIGHT_LIMIT_FIX - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - ISL - - LOD_BLENDING - - EXTENDED_TRANSLUCENCY - entries: - - entry: Lighting:Vertex:1 - defines: - - VC - - entry: Lighting:Vertex:48001 - defines: - - VC - - PROJECTED_UV - - WORLD_MAP - - entry: Lighting:Vertex:40001 - defines: - - VC - - WORLD_MAP - - entry: Lighting:Vertex:8001 - defines: - - VC - - PROJECTED_UV - - entry: Lighting:Vertex:C008001 - defines: - - VC - - PROJECTED_UV - - TREE_ANIM - - entry: Lighting:Vertex:C000001 - defines: - - VC - - TREE_ANIM - - entry: Lighting:Vertex:B000001 - defines: - - VC - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Vertex:1008001 - defines: - - VC - - PROJECTED_UV - - ENVMAP - - entry: Lighting:Vertex:1000001 - defines: - - VC - - ENVMAP - - entry: Lighting:Vertex:2 - defines: - - SKINNED - - entry: Lighting:Vertex:B000002 - defines: - - SKINNED - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Vertex:8002 - defines: - - SKINNED - - PROJECTED_UV - - entry: Lighting:Vertex:1000002 - defines: - - SKINNED - - ENVMAP - - entry: Lighting:Vertex:0 - defines: [] - - entry: Lighting:Vertex:48000 - defines: - - PROJECTED_UV - - WORLD_MAP - - entry: Lighting:Vertex:40000 - defines: - - WORLD_MAP - - entry: Lighting:Vertex:8000 - defines: - - PROJECTED_UV - - entry: Lighting:Vertex:B000000 - defines: - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Vertex:1008000 - defines: - - PROJECTED_UV - - ENVMAP - - entry: Lighting:Vertex:1000000 - defines: - - ENVMAP - - entry: Lighting:Vertex:3 - defines: - - VC - - SKINNED - - entry: Lighting:Vertex:10000003 - defines: - - VC - - SKINNED - - EYE - - entry: Lighting:Vertex:C000003 - defines: - - VC - - SKINNED - - TREE_ANIM - - entry: Lighting:Vertex:B000003 - defines: - - VC - - SKINNED - - MULTI_LAYER_PARALLAX - - ENVMAP - - entry: Lighting:Vertex:8003 - defines: - - VC - - SKINNED - - PROJECTED_UV - - entry: Lighting:Vertex:1000003 - defines: - - VC - - SKINNED - - ENVMAP - - entry: Lighting:Vertex:4 - defines: - - MODELSPACENORMALS - - entry: Lighting:Vertex:12000004 - defines: - - MODELSPACENORMALS - - LODLANDSCAPE - - LODLANDNOISE - - entry: Lighting:Vertex:9000004 - defines: - - MODELSPACENORMALS - - LODLANDSCAPE - - entry: Lighting:Vertex:5 - defines: - - VC - - MODELSPACENORMALS - - entry: Lighting:Vertex:6 - defines: - - SKINNED - - MODELSPACENORMALS - - entry: Lighting:Vertex:7 - defines: - - VC - - SKINNED - - MODELSPACENORMALS - - file: Water.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - FOG - - SSS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - ISL - - WATER - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - entries: - - entry: Water:Pixel:2001 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Pixel:3001 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:4001 - defines: - - VC - - UNDERWATER - - entry: Water:Pixel:5001 - defines: - - VC - - STENCIL - - entry: Water:Pixel:1 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:3801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Pixel:4801 - defines: - - VC - - LOD - - entry: Water:Pixel:801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:442 - defines: - - NORMAL_TEXCOORD - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1842 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2842 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:1042 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Pixel:2042 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Pixel:3042 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:42 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1002 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Pixel:2002 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Pixel:4002 - defines: - - NORMAL_TEXCOORD - - UNDERWATER - - entry: Water:Pixel:4009 - defines: - - VC - - REFRACTIONS - - UNDERWATER - - entry: Water:Pixel:9 - defines: - - VC - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:83 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1043 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Pixel:3043 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:43 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1843 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2843 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:843 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:4A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:44A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:19 - defines: - - VC - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:400B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - UNDERWATER - - entry: Water:Pixel:1000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Pixel:2000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Pixel:3000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:4000 - defines: - - UNDERWATER - - entry: Water:Pixel:5000 - defines: - - STENCIL - - entry: Water:Pixel:0 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:3803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Pixel:5803 - defines: - - VC - - NORMAL_TEXCOORD - - SIMPLE - - entry: Water:Pixel:4803 - defines: - - VC - - NORMAL_TEXCOORD - - LOD - - entry: Water:Pixel:803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:2003 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Pixel:3003 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:4003 - defines: - - VC - - NORMAL_TEXCOORD - - UNDERWATER - - entry: Water:Pixel:3 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:11 - defines: - - VC - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:211 - defines: - - VC - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:203 - defines: - - VC - - NORMAL_TEXCOORD - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:213 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:411 - defines: - - VC - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:413 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:601 - defines: - - VC - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:611 - defines: - - VC - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:603 - defines: - - VC - - NORMAL_TEXCOORD - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:613 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:401 - defines: - - VC - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:202 - defines: - - NORMAL_TEXCOORD - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:82 - defines: - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:9A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:9B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:283 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:21B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:21A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:61A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:61B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:683 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:69A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:29A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:282 - defines: - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4008 - defines: - - REFRACTIONS - - UNDERWATER - - entry: Water:Pixel:8 - defines: - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:400A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - UNDERWATER - - entry: Water:Pixel:A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:12 - defines: - - NORMAL_TEXCOORD - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:18 - defines: - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:3040 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:40 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:1041 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Pixel:3041 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Pixel:4048 - defines: - - REFRACTIONS - - WADING - - UNDERWATER - - entry: Water:Pixel:4049 - defines: - - VC - - REFRACTIONS - - WADING - - UNDERWATER - - entry: Water:Pixel:49 - defines: - - VC - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:50 - defines: - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:51 - defines: - - VC - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:52 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:58 - defines: - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:59 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:5A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:5B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:80 - defines: - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:88 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:8A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:90 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:91 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:92 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:93 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:98 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:99 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:D8 - defines: - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:DB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:200 - defines: - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4208 - defines: - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:209 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:420A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:20A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:420B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:20B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:210 - defines: - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:218 - defines: - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:241 - defines: - - VC - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:243 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4248 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:248 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4249 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:249 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:424A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:24A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:424B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Pixel:24B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:250 - defines: - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:251 - defines: - - VC - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:252 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:258 - defines: - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:259 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:25B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:280 - defines: - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:289 - defines: - - VC - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:28A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:28B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:291 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:292 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:293 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:298 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:299 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2D8 - defines: - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2DB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:400 - defines: - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:402 - defines: - - NORMAL_TEXCOORD - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:408 - defines: - - REFRACTIONS - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:409 - defines: - - VC - - REFRACTIONS - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:40A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:410 - defines: - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:418 - defines: - - REFRACTIONS - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:419 - defines: - - VC - - REFRACTIONS - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:41A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:41B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:440 - defines: - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:441 - defines: - - VC - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:451 - defines: - - VC - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:452 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:453 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:458 - defines: - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:459 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:45A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:45B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:480 - defines: - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:481 - defines: - - VC - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:482 - defines: - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:483 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:488 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:48B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:491 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:492 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:493 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:498 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:499 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:49B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C8 - defines: - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4D2 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4D8 - defines: - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4DB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:600 - defines: - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4608 - defines: - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:4609 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:609 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:460A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:60A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:460B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:610 - defines: - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:618 - defines: - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:619 - defines: - - VC - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:640 - defines: - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:641 - defines: - - VC - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:643 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4648 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:648 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:4649 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:464A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:64A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:464B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Pixel:64B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:651 - defines: - - VC - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:652 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:653 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:658 - defines: - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:65A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:65B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:680 - defines: - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:681 - defines: - - VC - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:682 - defines: - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:689 - defines: - - VC - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:68B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:690 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:692 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:698 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:699 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C8 - defines: - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6D2 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:6DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Pixel:2800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:3800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Pixel:4800 - defines: - - LOD - - entry: Water:Pixel:800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:1802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:3802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Pixel:4802 - defines: - - NORMAL_TEXCOORD - - LOD - - entry: Water:Pixel:802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:1840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Pixel:1841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Pixel:2841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Pixel:841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - WETNESS_EFFECTS - - CLOUD_SHADOWS - - TERRAIN_SHADOWS - - DYNAMIC_CUBEMAPS - - FOG - - SSS - - WATER_EFFECTS - - IBL - - SKYLIGHTING - - ISL - - WATER - - LOD_BLENDING - - LIGHT_LIMIT_FIX - - SCREEN_SPACE_SHADOWS - entries: - - entry: Water:Vertex:1001 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:2001 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Vertex:4001 - defines: - - VC - - UNDERWATER - - entry: Water:Vertex:5001 - defines: - - VC - - STENCIL - - entry: Water:Vertex:1 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:3801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Vertex:5801 - defines: - - VC - - SIMPLE - - entry: Water:Vertex:801 - defines: - - VC - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1842 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2842 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:842 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1042 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:2042 - defines: - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Vertex:2002 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Vertex:3002 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Vertex:4002 - defines: - - NORMAL_TEXCOORD - - UNDERWATER - - entry: Water:Vertex:4009 - defines: - - VC - - REFRACTIONS - - UNDERWATER - - entry: Water:Vertex:443 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1043 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:3043 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Vertex:43 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1843 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:843 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:404A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - UNDERWATER - - entry: Water:Vertex:4A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:400B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - UNDERWATER - - entry: Water:Vertex:B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:2000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Vertex:3000 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Vertex:4000 - defines: - - UNDERWATER - - entry: Water:Vertex:5000 - defines: - - STENCIL - - entry: Water:Vertex:0 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:3803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Vertex:5803 - defines: - - VC - - NORMAL_TEXCOORD - - SIMPLE - - entry: Water:Vertex:4803 - defines: - - VC - - NORMAL_TEXCOORD - - LOD - - entry: Water:Vertex:803 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1003 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:3003 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Vertex:2003 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=4 - - entry: Water:Vertex:5003 - defines: - - VC - - NORMAL_TEXCOORD - - STENCIL - - entry: Water:Vertex:3 - defines: - - VC - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:201 - defines: - - VC - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:211 - defines: - - VC - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:203 - defines: - - VC - - NORMAL_TEXCOORD - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:213 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:411 - defines: - - VC - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:413 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:601 - defines: - - VC - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:613 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:202 - defines: - - NORMAL_TEXCOORD - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:9A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:9B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:29B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:602 - defines: - - NORMAL_TEXCOORD - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:283 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:21B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:21A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:61A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:61B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:683 - defines: - - VC - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:69A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:29A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:282 - defines: - - NORMAL_TEXCOORD - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:8 - defines: - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:400A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - UNDERWATER - - entry: Water:Vertex:A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:12 - defines: - - NORMAL_TEXCOORD - - DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1040 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=2 - - entry: Water:Vertex:3041 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=6 - - entry: Water:Vertex:41 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4048 - defines: - - REFRACTIONS - - WADING - - UNDERWATER - - entry: Water:Vertex:4049 - defines: - - VC - - REFRACTIONS - - WADING - - UNDERWATER - - entry: Water:Vertex:49 - defines: - - VC - - REFRACTIONS - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:51 - defines: - - VC - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:53 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:59 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:5A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:81 - defines: - - VC - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:88 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:89 - defines: - - VC - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:8A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:8B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:90 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:91 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:92 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:93 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:99 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:C8 - defines: - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D2 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D8 - defines: - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:DB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4208 - defines: - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:208 - defines: - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4209 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:209 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:420A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:20A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:420B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:20B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:210 - defines: - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:212 - defines: - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:218 - defines: - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:219 - defines: - - VC - - REFRACTIONS - - DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:240 - defines: - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:241 - defines: - - VC - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:242 - defines: - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:243 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4248 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:248 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4249 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:249 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:424A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:24A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:424B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - UNDERWATER - - entry: Water:Vertex:250 - defines: - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:252 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:258 - defines: - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:259 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:25B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:280 - defines: - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:281 - defines: - - VC - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:288 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:289 - defines: - - VC - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:28A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:28B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:290 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:291 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:292 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:293 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:298 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:299 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2C8 - defines: - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2D2 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:2DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:400 - defines: - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:409 - defines: - - VC - - REFRACTIONS - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:40B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:410 - defines: - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:412 - defines: - - NORMAL_TEXCOORD - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:418 - defines: - - REFRACTIONS - - DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:440 - defines: - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:448 - defines: - - REFRACTIONS - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:449 - defines: - - VC - - REFRACTIONS - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:450 - defines: - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:451 - defines: - - VC - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:452 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:453 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:458 - defines: - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:459 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:45A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:45B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:480 - defines: - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:488 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:48A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:48B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:490 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:491 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:492 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:493 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:498 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:499 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:49B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D2 - defines: - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D8 - defines: - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4D9 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:600 - defines: - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4608 - defines: - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:608 - defines: - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4609 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:609 - defines: - - VC - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:60A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:460B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:60B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:610 - defines: - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:612 - defines: - - NORMAL_TEXCOORD - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:618 - defines: - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:619 - defines: - - VC - - REFRACTIONS - - DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:640 - defines: - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:641 - defines: - - VC - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:642 - defines: - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:643 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4648 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:648 - defines: - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:4649 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:649 - defines: - - VC - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:464A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:64A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:464B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - UNDERWATER - - entry: Water:Vertex:64B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:650 - defines: - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:651 - defines: - - VC - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:653 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:658 - defines: - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:659 - defines: - - VC - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:65A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:681 - defines: - - VC - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:688 - defines: - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:689 - defines: - - VC - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:68A - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:68B - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:690 - defines: - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:691 - defines: - - VC - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:692 - defines: - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:693 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:698 - defines: - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:699 - defines: - - VC - - REFRACTIONS - - DEPTH - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C0 - defines: - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C1 - defines: - - VC - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C2 - defines: - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C3 - defines: - - VC - - NORMAL_TEXCOORD - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C8 - defines: - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6C9 - defines: - - VC - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6CA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6CB - defines: - - VC - - NORMAL_TEXCOORD - - REFRACTIONS - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6D0 - defines: - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6D1 - defines: - - VC - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6D3 - defines: - - VC - - NORMAL_TEXCOORD - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:6DA - defines: - - NORMAL_TEXCOORD - - REFRACTIONS - - DEPTH - - WADING - - VERTEX_ALPHA_DEPTH - - FLOWMAP - - BLEND_NORMALS - - SPECULAR - - NUM_SPECULAR_LIGHTS=0 - - entry: Water:Vertex:1800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:3800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Vertex:4800 - defines: - - LOD - - entry: Water:Vertex:800 - defines: - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:3802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=7 - - entry: Water:Vertex:5802 - defines: - - NORMAL_TEXCOORD - - SIMPLE - - entry: Water:Vertex:4802 - defines: - - NORMAL_TEXCOORD - - LOD - - entry: Water:Vertex:802 - defines: - - NORMAL_TEXCOORD - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:840 - defines: - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - entry: Water:Vertex:1841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=3 - - entry: Water:Vertex:2841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=5 - - entry: Water:Vertex:841 - defines: - - VC - - WADING - - SPECULAR - - NUM_SPECULAR_LIGHTS=1 - - file: Utility.hlsl - configs: - PSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - PSHADER - - SHADOWSPLITCOUNT=3 - entries: - - entry: Utility:Pixel:462102 - defines: - - TEXTURE - - FOCUS_SHADOW - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=3 - - entry: Utility:Pixel:262102 - defines: - - TEXTURE - - FOCUS_SHADOW - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=3 - - entry: Utility:Pixel:202102 - defines: - - TEXTURE - - FOCUS_SHADOW - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Pixel:1002002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:1000002 - defines: - - TEXTURE - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:862002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=3 - - entry: Utility:Pixel:800002 - defines: - - TEXTURE - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:462002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=3 - - entry: Utility:Pixel:422002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=1 - - entry: Utility:Pixel:262002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=3 - - entry: Utility:Pixel:222002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=1 - - entry: Utility:Pixel:80002 - defines: - - TEXTURE - - DEBUG_COLOR - - entry: Utility:Pixel:40002 - defines: - - TEXTURE - - DEBUG_SHADOWSPLIT - - entry: Utility:Pixel:14002 - defines: - - TEXTURE - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Pixel:12002 - defines: - - TEXTURE - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Pixel:1002 - defines: - - TEXTURE - - RENDER_NORMAL_CLEAR - - entry: Utility:Pixel:1062002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=3 - - entry: Utility:Pixel:2000002 - defines: - - TEXTURE - - RENDER_BASE_TEXTURE - - entry: Utility:Pixel:1022002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=1 - - entry: Utility:Pixel:1002083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:1000083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:800083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:422083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=1 - - entry: Utility:Pixel:400083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Pixel:202083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Pixel:200083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Pixel:40083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Pixel:2083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Pixel:6083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Pixel:4083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - entry: Utility:Pixel:20083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - DEBUG_COLOR - - entry: Utility:Pixel:21083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - DEBUG_COLOR - - entry: Utility:Pixel:22083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:2E083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:32083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:1022083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=1 - - entry: Utility:Pixel:2020083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - DEBUG_COLOR - - entry: Utility:Pixel:4002083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - TREE_ANIM - - entry: Utility:Pixel:400E083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - TREE_ANIM - - entry: Utility:Pixel:4012083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - TREE_ANIM - - entry: Utility:Pixel:4014083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - TREE_ANIM - - entry: Utility:Pixel:4102083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - GRAYSCALE_MASK - - TREE_ANIM - - entry: Utility:Pixel:4202083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Pixel:4400083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Pixel:4802083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Pixel:6000083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - TREE_ANIM - - entry: Utility:Pixel:8002083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - LOD_OBJECT - - entry: Utility:Pixel:2000 - defines: - - RENDER_DEPTH - - NO_PIXEL_SHADER - - entry: Utility:Pixel:1000003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:802003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:402003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Pixel:400003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Pixel:202003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Pixel:80003 - defines: - - VC - - TEXTURE - - DEBUG_COLOR - - entry: Utility:Pixel:40003 - defines: - - VC - - TEXTURE - - DEBUG_SHADOWSPLIT - - entry: Utility:Pixel:12003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Pixel:10003 - defines: - - VC - - TEXTURE - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Pixel:8003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Pixel:1003 - defines: - - VC - - TEXTURE - - RENDER_NORMAL_CLEAR - - entry: Utility:Pixel:14003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Pixel:2000003 - defines: - - VC - - TEXTURE - - RENDER_BASE_TEXTURE - - entry: Utility:Pixel:10000003 - defines: - - VC - - TEXTURE - - LOCALMAP_FOGOFWAR - - entry: Utility:Pixel:1002003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:A03 - defines: - - VC - - TEXTURE - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Pixel:203 - defines: - - VC - - TEXTURE - - RENDER_NORMAL - - entry: Utility:Pixel:1202 - defines: - - TEXTURE - - STENCIL_ABOVE_WATER - - entry: Utility:Pixel:1020082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=1 - - entry: Utility:Pixel:1000082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:800082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Pixel:420082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=1 - - entry: Utility:Pixel:400082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Pixel:220082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=1 - - entry: Utility:Pixel:200082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Pixel:122082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - GRAYSCALE_MASK - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:60082 - defines: - - TEXTURE - - ALPHA_TEST - - DEBUG_COLOR - - DEBUG_SHADOWSPLIT - - entry: Utility:Pixel:40082 - defines: - - TEXTURE - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Pixel:12082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Pixel:C082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Pixel:6082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Pixel:4082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - entry: Utility:Pixel:1082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Pixel:2082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Pixel:14082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Pixel:16082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Pixel:21082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - DEBUG_COLOR - - entry: Utility:Pixel:24082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:26082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:2C082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:2E082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:32082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:34082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:36082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - DEPTH_WRITE_DECALS - - entry: Utility:Pixel:2000082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - entry: Utility:Pixel:2020082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - DEBUG_COLOR - - entry: Utility:Pixel:8002082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - LOD_OBJECT - - entry: Utility:Pixel:283 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Pixel:20283 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - DEBUG_COLOR - - entry: Utility:Pixel:4000283 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - TREE_ANIM - - entry: Utility:Pixel:A02 - defines: - - TEXTURE - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Pixel:20A83 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - DEBUG_COLOR - - entry: Utility:Pixel:4000A83 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - TREE_ANIM - - entry: Utility:Pixel:20A82 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - DEBUG_COLOR - - entry: Utility:Pixel:A82 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Pixel:282 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Pixel:20282 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - DEBUG_COLOR - VSHADER: - common_defines: - - VR - - D3DCOMPILE_DEBUG - - D3DCOMPILE_SKIP_OPTIMIZATION - - VSHADER - - SHADOWSPLITCOUNT=3 - entries: - - entry: Utility:Vertex:402102 - defines: - - TEXTURE - - FOCUS_SHADOW - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:842002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=2 - - entry: Utility:Vertex:802002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:800002 - defines: - - TEXTURE - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:400002 - defines: - - TEXTURE - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:242002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=2 - - entry: Utility:Vertex:200002 - defines: - - TEXTURE - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:40002 - defines: - - TEXTURE - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:16002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:14002 - defines: - - TEXTURE - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1002 - defines: - - TEXTURE - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:12002 - defines: - - TEXTURE - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:1042002 - defines: - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=2 - - entry: Utility:Vertex:2000002 - defines: - - TEXTURE - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:800083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:400083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:202083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:40083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:83 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - entry: Utility:Vertex:1083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:2083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:4083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_SHADOWMAP - - entry: Utility:Vertex:E083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:1002083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:20002083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - OPAQUE_EFFECT - - entry: Utility:Vertex:2000A083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - OPAQUE_EFFECT - - GRAYSCALE_TO_ALPHA - - entry: Utility:Vertex:802083 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:80201B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:1201B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:80001B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40201B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:40001B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20201B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:20001B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:4001B - defines: - - VC - - TEXTURE - - NORMALS - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1601B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1401B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1B - defines: - - VC - - TEXTURE - - NORMALS - - entry: Utility:Vertex:801B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:101B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:1001B - defines: - - VC - - TEXTURE - - NORMALS - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:100201B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:200001B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:100001B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:4000 - defines: - - RENDER_SHADOWMAP - - NO_PIXEL_SHADER - - entry: Utility:Vertex:6000 - defines: - - RENDER_DEPTH - - RENDER_SHADOWMAP - - NO_PIXEL_SHADER - - entry: Utility:Vertex:C000 - defines: - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - NO_PIXEL_SHADER - - entry: Utility:Vertex:E000 - defines: - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - NO_PIXEL_SHADER - - entry: Utility:Vertex:2000 - defines: - - RENDER_DEPTH - - NO_PIXEL_SHADER - - entry: Utility:Vertex:1000003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:802003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:402003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:400003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:202003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:200003 - defines: - - VC - - TEXTURE - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:40003 - defines: - - VC - - TEXTURE - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:16003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1003 - defines: - - VC - - TEXTURE - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:3 - defines: - - VC - - TEXTURE - - entry: Utility:Vertex:12003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:1002003 - defines: - - VC - - TEXTURE - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:2000003 - defines: - - VC - - TEXTURE - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:10000003 - defines: - - VC - - TEXTURE - - LOCALMAP_FOGOFWAR - - entry: Utility:Vertex:6004 - defines: - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMAP - - NO_PIXEL_SHADER - - entry: Utility:Vertex:C004 - defines: - - SKINNED - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - NO_PIXEL_SHADER - - entry: Utility:Vertex:E004 - defines: - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - NO_PIXEL_SHADER - - entry: Utility:Vertex:2004 - defines: - - SKINNED - - RENDER_DEPTH - - NO_PIXEL_SHADER - - entry: Utility:Vertex:800006 - defines: - - TEXTURE - - SKINNED - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:400006 - defines: - - TEXTURE - - SKINNED - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:200006 - defines: - - TEXTURE - - SKINNED - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:14006 - defines: - - TEXTURE - - SKINNED - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1006 - defines: - - TEXTURE - - SKINNED - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:6 - defines: - - TEXTURE - - SKINNED - - entry: Utility:Vertex:1000007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:802007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:800007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:402007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:200007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:40007 - defines: - - VC - - TEXTURE - - SKINNED - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:16007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:1002007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:2000007 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:207 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_NORMAL - - entry: Utility:Vertex:603 - defines: - - VC - - TEXTURE - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:1202 - defines: - - TEXTURE - - STENCIL_ABOVE_WATER - - entry: Utility:Vertex:202 - defines: - - TEXTURE - - RENDER_NORMAL - - entry: Utility:Vertex:80001A - defines: - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40001A - defines: - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20001A - defines: - - TEXTURE - - NORMALS - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:1401A - defines: - - TEXTURE - - NORMALS - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1A - defines: - - TEXTURE - - NORMALS - - entry: Utility:Vertex:101A - defines: - - TEXTURE - - NORMALS - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:200001A - defines: - - TEXTURE - - NORMALS - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:100001A - defines: - - TEXTURE - - NORMALS - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:1201E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:20001E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:4001E - defines: - - TEXTURE - - SKINNED - - NORMALS - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1601E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1401E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1E - defines: - - TEXTURE - - SKINNED - - NORMALS - - entry: Utility:Vertex:101E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:200001E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:100001E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:80201F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:1201F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:40201F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20201F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:20001F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:4001F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1601F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1401F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - entry: Utility:Vertex:101F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:100201F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:200001F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:100001F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:20002082 - defines: - - TEXTURE - - ALPHA_TEST - - RENDER_DEPTH - - OPAQUE_EFFECT - - entry: Utility:Vertex:20002086 - defines: - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - OPAQUE_EFFECT - - entry: Utility:Vertex:2086 - defines: - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:800087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:402087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:400087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:202087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:200087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:40087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:16087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:12087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:87 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - entry: Utility:Vertex:2087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:4087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMAP - - entry: Utility:Vertex:6087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Vertex:C087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:E087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:1000087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:1002087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:2000087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:20002087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - OPAQUE_EFFECT - - entry: Utility:Vertex:802087 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40009A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:4009A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1609A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1409A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1209A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:9A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - entry: Utility:Vertex:109A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:209A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:409A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - entry: Utility:Vertex:C09A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:E09A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:100009A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:200009A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:80009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:40009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:4009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1609B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1409B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:9B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - entry: Utility:Vertex:209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:109B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:609B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Vertex:C09B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:100009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:100209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:200009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:400009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - TREE_ANIM - - entry: Utility:Vertex:400109B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - TREE_ANIM - - entry: Utility:Vertex:400209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - TREE_ANIM - - entry: Utility:Vertex:400409B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - TREE_ANIM - - entry: Utility:Vertex:400C09B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - TREE_ANIM - - entry: Utility:Vertex:401209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - TREE_ANIM - - entry: Utility:Vertex:401409B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - TREE_ANIM - - entry: Utility:Vertex:401609B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - TREE_ANIM - - entry: Utility:Vertex:404009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - TREE_ANIM - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:420009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASK - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:420209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:440009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:440209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKSPOT - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:480009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:500209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:600009B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - TREE_ANIM - - entry: Utility:Vertex:80209B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:4009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:1609E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1209E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:E09E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:9E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - entry: Utility:Vertex:109E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:209E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:609E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Vertex:C09E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:100009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:200009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - entry: Utility:Vertex:80009E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:80009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:40009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - SHADOWFILTER=0 - - entry: Utility:Vertex:20209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:20009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASK - - SHADOWFILTER=0 - - entry: Utility:Vertex:1609F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1409F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - entry: Utility:Vertex:1209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - entry: Utility:Vertex:E09F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:9F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - entry: Utility:Vertex:109F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - entry: Utility:Vertex:209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - entry: Utility:Vertex:609F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - entry: Utility:Vertex:C09F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - entry: Utility:Vertex:100009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:100209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - SHADOWFILTER=0 - - entry: Utility:Vertex:400009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - TREE_ANIM - - entry: Utility:Vertex:400109F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL_CLEAR - - TREE_ANIM - - entry: Utility:Vertex:400209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - TREE_ANIM - - entry: Utility:Vertex:400409F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - TREE_ANIM - - entry: Utility:Vertex:400609F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - TREE_ANIM - - entry: Utility:Vertex:400C09F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - TREE_ANIM - - entry: Utility:Vertex:400E09F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_CLAMPED - - TREE_ANIM - - entry: Utility:Vertex:401209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - ADDITIONAL_ALPHA_MASK - - TREE_ANIM - - entry: Utility:Vertex:401609F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMAP - - RENDER_SHADOWMAP_PB - - TREE_ANIM - - entry: Utility:Vertex:404009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - TREE_ANIM - - DEBUG_SHADOWSPLIT - - entry: Utility:Vertex:420009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASK - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:440009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKSPOT - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:480009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:480209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:500009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_SHADOWMASKDPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:500209F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_DEPTH - - RENDER_SHADOWMASKDPB - - TREE_ANIM - - SHADOWFILTER=0 - - entry: Utility:Vertex:600009F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_BASE_TEXTURE - - TREE_ANIM - - entry: Utility:Vertex:A9F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A9B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:4000A9B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - TREE_ANIM - - entry: Utility:Vertex:69F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:400069F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - TREE_ANIM - - entry: Utility:Vertex:69B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:400069B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - TREE_ANIM - - entry: Utility:Vertex:29F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Vertex:400029F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - TREE_ANIM - - entry: Utility:Vertex:29B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Vertex:400029B - defines: - - VC - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - TREE_ANIM - - entry: Utility:Vertex:2100 - defines: - - LOD_LANDSCAPE - - RENDER_DEPTH - - NO_PIXEL_SHADER - - entry: Utility:Vertex:602 - defines: - - TEXTURE - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:287 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Vertex:283 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Vertex:21E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL - - entry: Utility:Vertex:21B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_NORMAL - - entry: Utility:Vertex:21A - defines: - - TEXTURE - - NORMALS - - RENDER_NORMAL - - entry: Utility:Vertex:206 - defines: - - TEXTURE - - SKINNED - - RENDER_NORMAL - - entry: Utility:Vertex:61A - defines: - - TEXTURE - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:61B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:61E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:61F - defines: - - VC - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:683 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:69A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:29E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - entry: Utility:Vertex:69E - defines: - - TEXTURE - - SKINNED - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_FALLOFF - - entry: Utility:Vertex:A02 - defines: - - TEXTURE - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A06 - defines: - - TEXTURE - - SKINNED - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A07 - defines: - - VC - - TEXTURE - - SKINNED - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A1A - defines: - - TEXTURE - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A1B - defines: - - VC - - TEXTURE - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A1E - defines: - - TEXTURE - - SKINNED - - NORMALS - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A83 - defines: - - VC - - TEXTURE - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A87 - defines: - - VC - - TEXTURE - - SKINNED - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP - - entry: Utility:Vertex:A9A - defines: - - TEXTURE - - NORMALS - - ALPHA_TEST - - RENDER_NORMAL - - RENDER_NORMAL_CLAMP diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c0c3b0e730..46eee01327 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,7 +13,7 @@ This file provides Copilot-specific guidance while avoiding duplication of the c ## Project Overview -SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/AE/VR. Features runtime shader compilation, 25+ graphics features, and cross-platform Skyrim variant support. +SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/AE. Features runtime shader compilation, 25+ graphics features, and cross-platform Skyrim variant support. ## Environment and Build Essentials @@ -31,7 +31,7 @@ SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/A ### Primary Build Command (Windows) ```powershell -# ALL preset is primary - other presets (SE, AE, VR) are legacy +# ALL preset is primary - other presets (SE, AE) are legacy ./BuildRelease.bat ALL # Universal binary (recommended) ./BuildRelease.bat # Same as ALL (default) ``` @@ -60,7 +60,7 @@ git submodule update --init --recursive # If not cloned with --recursive **Flag potential problems before they occur:** - **Performance Impact**: Graphics features affect rendering performance - suggest user toggles -- **Runtime Compatibility**: Warn about SE/AE/VR compatibility issues, suggest `REL::RelocateMember()` patterns +- **Runtime Compatibility**: Warn about SE/AE compatibility issues, suggest `REL::RelocateMember()` patterns - **Buffer Conflicts**: Highlight GPU register conflicts, recommend hlslkit buffer scanning - **Security Risks**: Validate user input, prevent DirectX crashes from malformed configurations @@ -68,7 +68,7 @@ git submodule update --init --recursive # If not cloned with --recursive - **Complete Solutions**: No TODO/FIXME placeholders - provide fully functional code - **Performance Conscious**: Always consider GPU workload and user experience -- **Cross-Platform**: Ensure changes work across SE/AE/VR variants using runtime detection +- **Cross-Platform**: Ensure changes work across SE/AE variants using runtime detection - **Error Handling**: Include proper resource management and graceful degradation ## Architecture Quick Reference diff --git a/.github/workflows/_shared-build.yaml b/.github/workflows/_shared-build.yaml index ecf4319149..d7b71c3661 100644 --- a/.github/workflows/_shared-build.yaml +++ b/.github/workflows/_shared-build.yaml @@ -133,8 +133,6 @@ jobs: config: - name: "Flatrim" file: ".github/configs/shader-validation.yaml" - - name: "VR" - file: ".github/configs/shader-validation-vr.yaml" fail-fast: false steps: - name: Checkout code diff --git a/AI-INSTRUCTIONS.md b/AI-INSTRUCTIONS.md index 56b9ddfe66..9f26a29050 100644 --- a/AI-INSTRUCTIONS.md +++ b/AI-INSTRUCTIONS.md @@ -8,7 +8,7 @@ This file provides guidance for AI assistants working with the Skyrim Community - Build commands and development setup - Architecture overview and critical dependencies (CommonLibSSE-NG) -- Runtime targeting system for SE/AE/VR compatibility +- Runtime targeting system for SE/AE compatibility - Core architecture including Globals system and feature registry - Shader architecture (base shaders in `package/Shaders/`, feature shaders, compute shader patterns) - Development workflows and best practices @@ -18,7 +18,7 @@ This file provides guidance for AI assistants working with the Skyrim Community ### Project Type -SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/AE/VR. +SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/AE. ### Essential Commands @@ -28,7 +28,7 @@ SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/A ### Build Options -**Runtime Presets**: `ALL` (universal), `SE`, `AE`, `VR`, `PRE-AE`, `FLATRIM`, `ALL-TRACY` +**Runtime Presets**: `ALL` (universal), `SE`, `AE`, `ALL-TRACY` **CMake Options** (set in user preset): @@ -50,6 +50,6 @@ For full details about manual packaging targets (Package-Core, Package-AIO-Manua **Act as an experienced graphics programming and Skyrim modding expert.** -**Key Focus**: Performance impact awareness, runtime compatibility (SE/AE/VR), complete working solutions, DirectX/HLSL best practices. +**Key Focus**: Performance impact awareness, runtime compatibility (SE/AE), complete working solutions, DirectX/HLSL best practices. For detailed explanations, examples, and comprehensive guidance, refer to `.claude/CLAUDE.md`. diff --git a/CMakePresets.json b/CMakePresets.json index 70e9da7bb7..5248788d68 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -56,7 +56,7 @@ "cacheVariables": { "ENABLE_SKYRIM_AE": "ON", "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "ON", + "ENABLE_SKYRIM_VR": "OFF", "AUTO_PLUGIN_DEPLOYMENT": "OFF", "COMMONLIB_PREBUILT_MULTICONFIG": "ON" }, @@ -68,7 +68,7 @@ "cacheVariables": { "ENABLE_SKYRIM_AE": "ON", "ENABLE_SKYRIM_SE": "ON", - "ENABLE_SKYRIM_VR": "ON", + "ENABLE_SKYRIM_VR": "OFF", "AUTO_PLUGIN_DEPLOYMENT": "OFF", "COMMONLIB_PREBUILT_MULTICONFIG": "ON" }, diff --git a/CMakeUserPresets.json.template b/CMakeUserPresets.json.template index a316db4344..99bd69fa8d 100644 --- a/CMakeUserPresets.json.template +++ b/CMakeUserPresets.json.template @@ -8,7 +8,7 @@ "AUTO_PLUGIN_DEPLOYMENT": "ON" }, "environment": { - "CommunityShadersOutputDir": "F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data" + "CommunityShadersOutputDir": "F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data" }, "inherits": "ALL" } diff --git a/README.md b/README.md index 9a8128f28d..0b977f67f1 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,6 @@ Install them manually only if you want them in everywhere. - [Address Library for SKSE](https://www.nexusmods.com/skyrimspecialedition/mods/32444) - Needed for SSE/AE -- [VR Address Library for SKSEVR](https://www.nexusmods.com/skyrimspecialedition/mods/58101) - - Needed for VR ## Build Instructions diff --git a/containerbuild.ps1 b/containerbuild.ps1 index 80390fbb43..463e9aeaa6 100644 --- a/containerbuild.ps1 +++ b/containerbuild.ps1 @@ -9,7 +9,7 @@ Write-Host "Starting build..." if (-Not (Test-Path -Path "CMakeUserPresets.json")) { Copy-Item -Path "CMakeUserPresets.json.template" -Destination "CMakeUserPresets.json" - (Get-Content -Path "CMakeUserPresets.json") -replace 'F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data', 'C:/skyrim-community-shaders/build' | Set-Content -Path "CMakeUserPresets.json" + (Get-Content -Path "CMakeUserPresets.json") -replace 'F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data', 'C:/skyrim-community-shaders/build' | Set-Content -Path "CMakeUserPresets.json" Write-Host "CMakeUserPresets.json created and modified." } else { Write-Host "CMakeUserPresets.json already exists. No action taken." diff --git a/docs/development/README.md b/docs/development/README.md index 92cc9e7fff..d0701a2689 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -36,7 +36,7 @@ Use `tools/new-worktree.ps1` when creating a new worktree for development. The s Examples: - `pwsh ./tools/new-worktree.ps1 -Name reproj_fixes` -- `pwsh ./tools/new-worktree.ps1 -Name vr-debug -StartPoint dev` +- `pwsh ./tools/new-worktree.ps1 -Name shader-debug -StartPoint dev` - `pwsh ./tools/new-worktree.ps1 -Name clean-build -NoSubmodules` If you want a Git-native command, install the optional repo-local alias: diff --git a/docs/development/shader-workflow.md b/docs/development/shader-workflow.md index bf58da9431..1a95e89f84 100644 --- a/docs/development/shader-workflow.md +++ b/docs/development/shader-workflow.md @@ -17,7 +17,7 @@ pwsh tools/verify-shader-refactor.ps1 package/Shaders/Foo.hlsl # or tools/veri `tools/verify-shader-refactor.ps1` (bash wrapper: `tools/verify-shader-refactor.sh`) compiles a shader from a base git ref and from the working tree across the -`VR` × `HDR_OUTPUT` permutations, then compares the compiled bytecode. The base +`HDR_OUTPUT` permutations, then compares the compiled bytecode. The base ref's whole include tree is materialized (via `git archive`), so the base compiles against base-ref `.hlsli` headers and the working tree against working headers — a refactor that also edits a shared header is compared correctly, not masked: diff --git a/docs/new-feature-template/NewFeature.h b/docs/new-feature-template/NewFeature.h index 1ae1ce3d69..86c53cfdf3 100644 --- a/docs/new-feature-template/NewFeature.h +++ b/docs/new-feature-template/NewFeature.h @@ -38,7 +38,6 @@ struct NewFeature : public Feature } // Functionality - virtual bool inline SupportsVR() override { return true; } virtual inline std::string_view GetShaderDefineName() override { return "SHADER_MACRO"; } virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return t == RE::BSShader::Type::Lighting; }; diff --git a/docs/new-feature-template/NewFeatureReadme.md b/docs/new-feature-template/NewFeatureReadme.md index 85c06059fa..18c72cd546 100644 --- a/docs/new-feature-template/NewFeatureReadme.md +++ b/docs/new-feature-template/NewFeatureReadme.md @@ -60,13 +60,6 @@ YourFeature yourFeature{}; - Customize `DrawSettings()` UI controls - Update shader compilation paths in `CompileShaders()` -### VR Support - -Set `SupportsVR()` return value: - -- `return true;` - Feature works in VR -- `return false;` - Feature disabled in VR builds - ## Naming Conventions | Component | Convention | Example | @@ -86,7 +79,7 @@ The build system automatically handles: - Settings persistence (JSON serialization) - UI menu integration - Feature lifecycle management -- Cross-platform builds (SE/AE/VR) +- Cross-platform builds (SE/AE) Build with: `./BuildRelease.bat ALL` @@ -96,5 +89,4 @@ Build with: `./BuildRelease.bat ALL` - [ ] Settings save/load correctly - [ ] Shaders compile without errors - [ ] Feature works in-game -- [ ] VR compatibility (if enabled) - [ ] No build errors diff --git a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl index f6d535d635..b4ef83346f 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl @@ -1,7 +1,6 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" #include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" RWTexture2DArray DynamicCubemap : register(u0); RWTexture2DArray DynamicCubemapRaw : register(u1); @@ -73,7 +72,6 @@ float smoothbumpstep(float edge0, float edge1, float x) float weight = 0.0; uv = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); - uv = Stereo::ConvertToStereoUV(uv, 0); float depth = DepthTexture.SampleLevel(LinearSampler, uv, 0); float linearDepth = SharedData::GetScreenDepth(depth); @@ -84,7 +82,7 @@ float smoothbumpstep(float edge0, float edge1, float x) if (linearDepth > 16.5 && depth != 1.0) { // Ignore objects which are too close or the sky #endif half4 positionCS = half4(2 * half2(uv.x, -uv.y + 1) - 1, depth, 1); - positionCS = mul(FrameBuffer::CameraViewProjInverse[0], positionCS); + positionCS = mul(FrameBuffer::CameraViewProjInverse, positionCS); positionCS.xyz = positionCS.xyz / positionCS.w; position += positionCS.xyz; @@ -114,7 +112,7 @@ float smoothbumpstep(float edge0, float edge1, float x) } float4 position = DynamicCubemapPosition[ThreadID]; - position.xyz = (position.xyz + (CameraPreviousPosAdjust2.xyz * 0.001)) - (FrameBuffer::CameraPosAdjust[0].xyz * 0.001); // Remove adjustment, add new adjustment + position.xyz = (position.xyz + (CameraPreviousPosAdjust2.xyz * 0.001)) - (FrameBuffer::CameraPosAdjust.xyz * 0.001); // Remove adjustment, add new adjustment DynamicCubemapPosition[ThreadID] = position; float4 color = DynamicCubemapRaw[ThreadID]; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index c15a0280d3..ede4395433 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -23,15 +23,6 @@ namespace ExponentialHeightFog return SharedData::exponentialHeightFogSettings.enabled && SharedData::exponentialHeightFogSettings.disableVanillaFog != 0; } - uint GetEyeIndexFromCameraWS(float3 cameraWS) - { -#if defined(VR) - return distance(cameraWS, FrameBuffer::CameraPosAdjust[1].xyz) < distance(cameraWS, FrameBuffer::CameraPosAdjust[0].xyz) ? 1u : 0u; -#else - return 0u; -#endif - } - bool ShouldApplyVolumetricFog() { return SharedData::exponentialHeightFogSettings.enabled != 0 && @@ -44,9 +35,9 @@ namespace ExponentialHeightFog return max(clipPosition.w, SharedData::CameraData.y); } - float GetSceneDepthForFog(float3 positionWS, uint eyeIndex, out float2 volumeUV, out float projectedDepth) + float GetSceneDepthForFog(float3 positionWS, out float2 volumeUV, out float projectedDepth) { - float4 clipPosition = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1.0f)); + float4 clipPosition = mul(FrameBuffer::CameraViewProj, float4(positionWS, 1.0f)); [branch] if (clipPosition.w <= 0.0f) { volumeUV = 0.0f.xx; @@ -61,7 +52,7 @@ namespace ExponentialHeightFog return projectedDepth; } - float4 SampleVolumetricFog(float3 positionWS, uint eyeIndex) + float4 SampleVolumetricFog(float3 positionWS) { if (!ShouldApplyVolumetricFog()) return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -75,25 +66,15 @@ namespace ExponentialHeightFog float2 volumeUV; float projectedDepth; - float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); + float sceneDepth = GetSceneDepthForFog(positionWS, volumeUV, projectedDepth); if (projectedDepth <= 0.0f) return float4(0.0f, 0.0f, 0.0f, 1.0f); -#if defined(VR) - volumeUV = Stereo::ConvertToStereoUV(volumeUV, eyeIndex); -#endif - float volumeZ = saturate(ComputeVolumetricNormalizedSlice(sceneDepth, float(volumeDepth))); float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); float2 volumeUVMin = volumeTexelCenter.xy; float2 volumeUVMax = 1.0f.xx - volumeTexelCenter.xy; -#if defined(VR) - float eyeMinX = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; - float eyeMaxX = (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x; - volumeUVMin.x = eyeMinX; - volumeUVMax.x = eyeMaxX; -#endif float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); @@ -106,7 +87,7 @@ namespace ExponentialHeightFog return saturate(viewSizeSafe / physicalSize); } - float4 SampleVolumetricFog(float4 screenPosition, uint eyeIndex) + float4 SampleVolumetricFog(float4 screenPosition) { if (!ShouldApplyVolumetricFog()) return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -137,10 +118,6 @@ namespace ExponentialHeightFog float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); float2 volumeUVMin = volumeTexelCenter.xy; float2 volumeUVMax = max(GetVolumetricFogUVMax(volumeSize, gridPixelSize), volumeUVMin); -#if defined(VR) - volumeUVMin.x = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; - volumeUVMax.x = max(volumeUVMin.x, min(volumeUVMax.x, (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x)); -#endif float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); @@ -172,9 +149,9 @@ namespace ExponentialHeightFog return volumetricFog; } - float4 CombineVolumetricFog(float4 analyticalFog, float3 positionWS, uint eyeIndex, float3 viewDirection) + float4 CombineVolumetricFog(float4 analyticalFog, float3 positionWS, float3 viewDirection) { - float4 volumetricFog = SampleVolumetricFog(positionWS, eyeIndex); + float4 volumetricFog = SampleVolumetricFog(positionWS); volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); float analyticalTransmittance = 1.0f - analyticalFog.w; float combinedTransmittance = volumetricFog.a * analyticalTransmittance; @@ -184,9 +161,9 @@ namespace ExponentialHeightFog return float4(combinedOpacity > 1e-4f ? combinedPremultiplied / combinedOpacity : float3(0.0f, 0.0f, 0.0f), combinedOpacity); } - float4 CombineVolumetricFog(float4 analyticalFog, float4 screenPosition, uint eyeIndex, float3 viewDirection) + float4 CombineVolumetricFog(float4 analyticalFog, float4 screenPosition, float3 viewDirection) { - float4 volumetricFog = SampleVolumetricFog(screenPosition, eyeIndex); + float4 volumetricFog = SampleVolumetricFog(screenPosition); volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); float analyticalTransmittance = 1.0f - analyticalFog.w; float combinedTransmittance = volumetricFog.a * analyticalTransmittance; @@ -203,11 +180,10 @@ namespace ExponentialHeightFog if (fogDensity <= 0.0f) { return 0.0f; } - uint eyeIndex = GetEyeIndexFromCameraWS(cameraWS); float3 viewToPos = positionWS; float2 volumeUV; float projectedDepth; - float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); + float sceneDepth = GetSceneDepthForFog(positionWS, volumeUV, projectedDepth); [branch] if (projectedDepth > 1e-4f && sceneDepth > projectedDepth) { viewToPos *= sceneDepth / projectedDepth; @@ -276,7 +252,7 @@ namespace ExponentialHeightFog if (!applyVolumetricFog) { return analyticalFog; } - return useScreenPosition ? CombineVolumetricFog(analyticalFog, screenPosition, eyeIndex, viewDirection) : CombineVolumetricFog(analyticalFog, positionWS, eyeIndex, viewDirection); + return useScreenPosition ? CombineVolumetricFog(analyticalFog, screenPosition, viewDirection) : CombineVolumetricFog(analyticalFog, positionWS, viewDirection); } float4 GetExponentialHeightFog(float3 positionWS, float3 cameraWS, float3 fogColor) diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli index bdd69fb861..51aa39331b 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli @@ -2,14 +2,13 @@ #define __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_CS_COMMON_HLSLI__ #include "Common/FrameBuffer.hlsli" -#include "Common/VR.hlsli" cbuffer VolumetricFogCB : register(b0) { uint4 VolumetricFogGridSizeAndFlags; float4 VolumetricFogInvGridSizeAndNearFade; float4 VolumetricFogGridZParams; - row_major float4x4 VolumetricFogClipToWorld[2]; + row_major float4x4 VolumetricFogClipToWorld; float4 VolumetricFogFrameJitterOffsets[16]; float4 VolumetricFogHistoryParameters; float4 VolumetricFogJitterParameters; @@ -40,17 +39,15 @@ namespace ExponentialHeightFog return all(coord < VolumetricFogGridSize); } - float3 ComputeCellWorldPosition(uint3 coord, float3 cellOffset, out uint eyeIndex, out float viewDepth) + float3 ComputeCellWorldPosition(uint3 coord, float3 cellOffset, out float viewDepth) { float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; - eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUV); - float2 eyeUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); viewDepth = ComputeVolumetricSliceDepth(max(float(coord.z) + cellOffset.z, 0.0f)); - float2 ndc = eyeUV * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); + float2 ndc = volumeUV * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); float deviceZ = (SharedData::CameraData.x - SharedData::CameraData.w / viewDepth) / SharedData::CameraData.z; - float4 worldPosition = mul(VolumetricFogClipToWorld[eyeIndex], float4(ndc, deviceZ, 1.0f)); + float4 worldPosition = mul(VolumetricFogClipToWorld, float4(ndc, deviceZ, 1.0f)); return worldPosition.xyz / worldPosition.w; } } diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl index 7a87efa39e..f7f8cfad61 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl @@ -8,14 +8,12 @@ RWTexture2D ConservativeDepthTexture : register(u0); float2 volumeUVMin = (float2(dispatchID.xy) - 0.5f.xx) * VolumetricFogInvGridSize.xy; float2 volumeUVMax = (float2(dispatchID.xy + 1u) + 0.5f.xx) * VolumetricFogInvGridSize.xy; - float2 volumeUVCenter = (float2(dispatchID.xy) + 0.5f.xx) * VolumetricFogInvGridSize.xy; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUVCenter); - float2 eyeUVMin = saturate(Stereo::ConvertFromStereoUV(volumeUVMin, eyeIndex)); - float2 eyeUVMax = saturate(Stereo::ConvertFromStereoUV(volumeUVMax, eyeIndex)); + float2 eyeUVMin = saturate(volumeUVMin); + float2 eyeUVMax = saturate(volumeUVMax); - int2 minCoord = SharedData::ConvertUVToSampleCoord(min(eyeUVMin, eyeUVMax), eyeIndex).xy; - int2 maxCoord = SharedData::ConvertUVToSampleCoord(max(eyeUVMin, eyeUVMax), eyeIndex).xy - 1; + int2 minCoord = SharedData::ConvertUVToSampleCoord(min(eyeUVMin, eyeUVMax)).xy; + int2 maxCoord = SharedData::ConvertUVToSampleCoord(max(eyeUVMin, eyeUVMax)).xy - 1; maxCoord = max(maxCoord, minCoord); int2 bufferMax = int2(SharedData::BufferDim.xy) - 1; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl index e009d7885b..16da9896bc 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl @@ -11,18 +11,16 @@ RWTexture3D IntegratedLightScattering : register(u0); float accumulatedTransmittance = 1.0f; float accumulatedDepth = 0.0f; - uint eyeIndex; float previousDepth; - float3 previousPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(uint3(dispatchID.xy, 0), float3(0.5f, 0.5f, 0.0f), eyeIndex, previousDepth); + float3 previousPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(uint3(dispatchID.xy, 0), float3(0.5f, 0.5f, 0.0f), previousDepth); [loop] for (uint layerIndex = 0; layerIndex < VolumetricFogGridSize.z; layerIndex++) { uint3 layerCoordinate = uint3(dispatchID.xy, layerIndex); float4 scatteringAndExtinction = LightScattering[layerCoordinate]; - uint layerEyeIndex; float layerDepth; - float3 layerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(layerCoordinate, 0.5f.xxx, layerEyeIndex, layerDepth); + float3 layerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(layerCoordinate, 0.5f.xxx, layerDepth); float stepLength = length(layerPositionWS - previousPositionWS); previousPositionWS = layerPositionWS; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl index 9263c88ba8..216922ce54 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl @@ -68,10 +68,10 @@ bool IsFroxelBehindSceneDepth(uint3 coord) return sceneDepth < frontDepth; } -float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, uint eyeIndex, out bool validHistory, out float previousViewDepth) +float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, out bool validHistory, out float previousViewDepth) { - float3 previousPositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPreviousPosAdjust[eyeIndex].xyz; - float4 previousClip = mul(FrameBuffer::CameraPreviousViewProjUnjittered[eyeIndex], float4(previousPositionWS, 1.0f)); + float3 previousPositionWS = positionWS + FrameBuffer::CameraPosAdjust.xyz - FrameBuffer::CameraPreviousPosAdjust.xyz; + float4 previousClip = mul(FrameBuffer::CameraPreviousViewProjUnjittered, float4(previousPositionWS, 1.0f)); previousViewDepth = abs(previousClip.w); validHistory = previousClip.w > 0.0f; @@ -79,9 +79,6 @@ float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, uint eyeIndex, out bool return 0.0f.xxx; float2 historyUV = previousClip.xy / previousClip.w * float2(0.5f, -0.5f) + 0.5f; -#if defined(VR) - historyUV = Stereo::ConvertToStereoUV(historyUV, eyeIndex); -#endif float historyZ = ExponentialHeightFog::ComputeVolumetricNormalizedSlice(previousViewDepth); float3 volumeUV = float3(historyUV, historyZ); @@ -89,10 +86,10 @@ float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, uint eyeIndex, out bool return saturate(volumeUV); } -float3 ComputeHistoryVolumeUV(float3 positionWS, uint eyeIndex, out bool validHistory) +float3 ComputeHistoryVolumeUV(float3 positionWS, out bool validHistory) { float previousViewDepth; - return ComputeHistoryVolumeUVAndDepth(positionWS, eyeIndex, validHistory, previousViewDepth); + return ComputeHistoryVolumeUVAndDepth(positionWS, validHistory, previousViewDepth); } float2 FixupHistoryUV(float2 uv, float previousCellDepth, out bool validHistory) @@ -159,7 +156,7 @@ float SampleDirectionalShadowPCF(float3 positionLS, uint cascadeIndex) return (center * 4.0f + cross) * rcp(8.0f); } -float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) +float SampleDirectionalShadow(float3 positionWS) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0f; @@ -167,7 +164,7 @@ float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) return 1.0f; DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(positionWS, eyeIndex)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(positionWS)); if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) return 1.0f; @@ -175,7 +172,7 @@ float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) float cascadeSelect = smoothstep(0.0f, 1.0f, saturate((shadowMapDepth - directionalShadowLightData.StartSplitDistances.y) / splitDenom)); uint primaryCascade = (uint)cascadeSelect; - float3 absolutePositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 absolutePositionWS = positionWS + FrameBuffer::CameraPosAdjust.xyz; float3 positionLS = mul(directionalShadowLightData.ShadowProj[primaryCascade], float4(absolutePositionWS, 1.0f)).xyz; if (any(positionLS.xy < 0.0f) || any(positionLS.xy > 1.0f)) return 1.0f; @@ -197,14 +194,14 @@ float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) return lerp(1.0f, shadow, fadeFactor); } -float SampleDirectionalWorldShadow(float3 positionWS, uint eyeIndex) +float SampleDirectionalWorldShadow(float3 positionWS) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0f; float worldShadow = 1.0f; #if defined(TERRAIN_SHADOWS) - worldShadow *= TerrainShadows::GetTerrainShadow(positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, LinearSampler); + worldShadow *= TerrainShadows::GetTerrainShadow(positionWS + FrameBuffer::CameraPosAdjust.xyz, LinearSampler); #endif #if defined(CLOUD_SHADOWS) worldShadow *= CloudShadows::GetCloudShadowMult(positionWS, LinearSampler); @@ -212,18 +209,14 @@ float SampleDirectionalWorldShadow(float3 positionWS, uint eyeIndex) return worldShadow; } -float3 ComputeSkyLightScattering(float3 positionWS, float3 viewDirection, uint eyeIndex) +float3 ComputeSkyLightScattering(float3 positionWS, float3 viewDirection) { float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; float3 skyDirection = abs(phaseG) > 0.001f ? normalize(-viewDirection * phaseG) : 0.0f.xxx; float3 skyVisibilityDirection = abs(phaseG) > 0.001f ? skyDirection : float3(0.0f, 0.0f, 1.0f); float skyVisibility = 1.0f; if (VolumetricFogHasSkylighting && !SharedData::InInterior) { -#if defined(VR) - float3 skylightingPosition = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -#else float3 skylightingPosition = positionWS; -#endif sh2 skylightingSH = Skylighting::SampleNoBias(skylightingPosition); skyVisibility = Skylighting::EvaluateDiffuse(skylightingSH, skyVisibilityDirection, Skylighting::GetFadeOutFactor(skylightingPosition)); } @@ -258,14 +251,13 @@ float3 AccumulateLocalLightScattering( float3 positionWS, float viewDepth, float3 viewDirection, - uint eyeIndex, float3 materialScattering) { if (!VolumetricFogHasLocalLights) return 0.0f.xxx; float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; - float2 screenUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); + float2 screenUV = volumeUV; uint clusterIndex = 0; if (!LightLimitFix::GetClusterIndex(screenUV, viewDepth, clusterIndex)) @@ -274,9 +266,8 @@ float3 AccumulateLocalLightScattering( LightLimitFix::LightGrid grid = LightLimitFix::lightGrid[clusterIndex]; uint lightCount = min(grid.lightCount, (uint)MAX_CLUSTER_LIGHTS); - uint cornerEyeIndex; float cornerViewDepth; - float3 cellCornerWS = ExponentialHeightFog::ComputeCellWorldPosition(coord + uint3(1, 1, 1), cellOffset, cornerEyeIndex, cornerViewDepth); + float3 cellCornerWS = ExponentialHeightFog::ComputeCellWorldPosition(coord + uint3(1, 1, 1), cellOffset, cornerViewDepth); float cellRadius = max(length(cellCornerWS - positionWS), 1.0f); float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; @@ -289,7 +280,7 @@ float3 AccumulateLocalLightScattering( if (light.lightFlags & LightLimitFix::LightFlags::Disabled) continue; - float3 toLight = light.positionWS[eyeIndex].xyz - positionWS; + float3 toLight = light.positionWS.xyz - positionWS; float distanceSqr = dot(toLight, toLight); if (distanceSqr < 1e-6f) continue; @@ -317,7 +308,6 @@ float3 AccumulateLocalLightScattering( float3 positionWS, float viewDepth, float3 viewDirection, - uint eyeIndex, float3 materialScattering) { return 0.0f.xxx; @@ -326,9 +316,8 @@ float3 AccumulateLocalLightScattering( float4 ComputeLightScattering(uint3 coord, float3 cellOffset) { - uint eyeIndex; float viewDepth; - float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(coord, cellOffset, eyeIndex, viewDepth); + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(coord, cellOffset, viewDepth); float4 materialScatteringAndExtinction = VBufferA[coord]; float extinction = materialScatteringAndExtinction.w; @@ -340,8 +329,8 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) // resolution during compositing in SampleVolumetricFog(). float directionalPhase = 1.0f / (4.0f * Math::PI); - float directionalShadow = SampleDirectionalShadow(positionWS, eyeIndex) * - SampleDirectionalWorldShadow(positionWS, eyeIndex); + float directionalShadow = SampleDirectionalShadow(positionWS) * + SampleDirectionalWorldShadow(positionWS); float3 directionalScattering = SharedData::DirLightColor.xyz * SharedData::exponentialHeightFogSettings.volumetricDirectionalScatteringIntensity * @@ -349,7 +338,7 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) directionalPhase * materialScatteringAndExtinction.rgb; - float3 skyScattering = ComputeSkyLightScattering(positionWS, viewDirection, eyeIndex) * + float3 skyScattering = ComputeSkyLightScattering(positionWS, viewDirection) * materialScatteringAndExtinction.rgb; float3 localScattering = AccumulateLocalLightScattering( @@ -358,7 +347,6 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) positionWS, viewDepth, viewDirection, - eyeIndex, materialScatteringAndExtinction.rgb); float3 emissive = SharedData::exponentialHeightFogSettings.volumetricFogEmissive.rgb * @@ -372,23 +360,21 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) return; - uint eyeIndex; float viewDepth; - float3 centerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); + float3 centerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, viewDepth); if (VolumetricFogHasConservativeDepth && IsFroxelBehindSceneDepth(dispatchID)) { LightScattering[dispatchID] = 0.0f.xxxx; return; } bool validHistory; - float3 historyUV = ComputeHistoryVolumeUV(centerPositionWS, eyeIndex, validHistory); + float3 historyUV = ComputeHistoryVolumeUV(centerPositionWS, validHistory); if (VolumetricFogHasPrevConservativeDepth && validHistory) { - uint frontEyeIndex; float frontDepth; - float3 frontPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, float3(0.5f, 0.5f, -0.5f), frontEyeIndex, frontDepth); + float3 frontPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, float3(0.5f, 0.5f, -0.5f), frontDepth); bool validFrontHistory; float previousFrontDepth; - ComputeHistoryVolumeUVAndDepth(frontPositionWS, frontEyeIndex, validFrontHistory, previousFrontDepth); + ComputeHistoryVolumeUVAndDepth(frontPositionWS, validFrontHistory, previousFrontDepth); if (validFrontHistory) { historyUV.xy = saturate(FixupHistoryUV(historyUV.xy, previousFrontDepth, validHistory)); } else { diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl index 07687f7015..7375d1fed5 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl @@ -6,11 +6,10 @@ RWTexture3D VBufferA : register(u0); if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) return; - uint eyeIndex; float viewDepth; - float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, viewDepth); - float extinction = ExponentialHeightFog::EvaluateHeightFogExtinction(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + float extinction = ExponentialHeightFog::EvaluateHeightFogExtinction(positionWS, FrameBuffer::CameraPosAdjust.xyz); float3 albedo = saturate(SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.rgb); float3 scattering = extinction * albedo * SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.a; diff --git a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli index f66859c9da..07631d0038 100644 --- a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli +++ b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli @@ -46,10 +46,6 @@ namespace ExtendedMaterials textureDims /= 2.0; #endif -#if defined(VR) - textureDims /= 2.0; -#endif - float2 texCoordsPerSize = coords * textureDims; float2 dxSize = ddx(texCoordsPerSize); @@ -68,11 +64,6 @@ namespace ExtendedMaterials mipLevel++; #endif -// VR: Apply more conservative mipmap level adjustments to reduce over-blurring and shimmering -#if defined(VR) - mipLevel++; -#endif - // Stochastic mip selection: use screen noise to select between adjacent mip levels mipLevel = floor(mipLevel) + (screenNoise < frac(mipLevel) ? 1.0 : 0.0); @@ -321,23 +312,12 @@ namespace ExtendedMaterials StochasticOffsets sharedOffset, float2 dx, float2 dy, # endif out float pixelOffset, -# if defined(VR_STEREO_OPT) - out bool hasPOM, -# endif out float weights[6]) #else - float2 GetParallaxCoords(float distance, float2 coords, float mipLevel, float3 viewDir, float3x3 tbn, float noise, Texture2D tex, SamplerState texSampler, uint channel, DisplacementParams params, out float pixelOffset -# if defined(VR_STEREO_OPT) - , - out bool hasPOM -# endif - ) + float2 GetParallaxCoords(float distance, float2 coords, float mipLevel, float3 viewDir, float3x3 tbn, float noise, Texture2D tex, SamplerState texSampler, uint channel, DisplacementParams params, out float pixelOffset) #endif { pixelOffset = 0.0; -#if defined(VR_STEREO_OPT) - hasPOM = false; -#endif float3 viewDirTS = normalize(mul(tbn, viewDir)); #if defined(LANDSCAPE) viewDirTS.xy /= viewDirTS.z * 0.7 + 0.3 + params[0].FlattenAmount; // Fix for objects at extreme viewing angles @@ -510,9 +490,6 @@ namespace ExtendedMaterials nearBlendToFar *= nearBlendToFar; float offset = (1.0 - parallaxAmount) * -maxHeight + minHeight; pixelOffset = saturate(lerp(parallaxAmount, 0.5, nearBlendToFar)); -#if defined(VR_STEREO_OPT) - hasPOM = true; -#endif return lerp(viewDirTS.xy * offset + coords.xy, coords, nearBlendToFar); } diff --git a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli index 9a751f07cc..c69af8508b 100644 --- a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli +++ b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli @@ -135,11 +135,11 @@ namespace GrassCollision void GetDisplacedPosition(VS_INPUT input, float3 position, out float3 displacement, out float3 previousDisplacement) { - float3 worldPosition = mul(World[0], float4(position.xyz, 1.0)).xyz; + float3 worldPosition = mul(World, float4(position.xyz, 1.0)).xyz; float nearFactor = smoothstep(2048.0, 0.0, length(worldPosition)); if (input.Color.w > 0.0 && nearFactor > 0.0) { - float3 worldPositionCentre = mul(World[0], float4(input.InstanceData1.xyz, 1.0)).xyz; + float3 worldPositionCentre = mul(World, float4(input.InstanceData1.xyz, 1.0)).xyz; // Limit stretching float3 remappedWorldPosition = lerp(worldPosition, worldPositionCentre, float3(0.95, 0.95, 0.0)); diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index d7d1ea07bb..15abfbed67 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -261,7 +261,7 @@ namespace Hair return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); } - float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) + float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise) { if (!SharedData::hairSpecularSettings.EnableSelfShadow) { return 1.0; @@ -270,8 +270,8 @@ namespace Hair // Simple raymarch const int stepCount = 4; - float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); - float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); + float3 positionVS = FrameBuffer::WorldToView(positionWS); + float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false); lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); float stepSize = 1.0 / stepCount; @@ -282,11 +282,11 @@ namespace Hair [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) { ray += lightDirVS * stepSize; - float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); + float2 rayUV = FrameBuffer::ViewToUV(ray); if (FrameBuffer::IsOutsideFrame(rayUV)) continue; float rayDepth = ray.z; - float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); + float sampleDepth = SharedData::GetScreenDepth(rayUV); if (sampleDepth < rayDepth) { hitCount++; } diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl index 8d0134aafa..da0f1d87b4 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl @@ -10,14 +10,14 @@ cbuffer PerFrame : register(b0) uint4 ClusterSize; } -float3 GetPositionVS(float2 texcoord, float depth, int eyeIndex = 0) +float3 GetPositionVS(float2 texcoord, float depth) { float4 clipSpaceLocation; clipSpaceLocation.xy = texcoord * 2.0f - 1.0f; // convert from [0,1] to [-1,1] clipSpaceLocation.y *= -1; clipSpaceLocation.z = depth; clipSpaceLocation.w = 1.0f; - float4 homogenousLocation = mul(FrameBuffer::CameraProjInverse[eyeIndex], clipSpaceLocation); + float4 homogenousLocation = mul(FrameBuffer::CameraProjInverse, clipSpaceLocation); return homogenousLocation.xyz / homogenousLocation.w; } @@ -52,13 +52,8 @@ float3 IntersectionZPlane(float3 B, float z_dist) float2 texcoordMax = (groupId.xy + 1) * clusterSize; float2 texcoordMin = groupId.xy * clusterSize; -#if !defined(VR) float3 maxPointVS = GetPositionVS(texcoordMax, 1.0f); float3 minPointVS = GetPositionVS(texcoordMin, 1.0f); -#else - float3 maxPointVS = max(GetPositionVS(texcoordMax, 1.0f, 0), GetPositionVS(texcoordMax, 1.0f, 1)); - float3 minPointVS = min(GetPositionVS(texcoordMin, 1.0f, 0), GetPositionVS(texcoordMin, 1.0f, 1)); -#endif // !VR float clusterNear = LightsNear * pow(abs(LightsFar / LightsNear), groupId.z / float(ClusterSize.z)); float clusterFar = LightsNear * pow(abs(LightsFar / LightsNear), (groupId.z + 1) / float(ClusterSize.z)); diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl index 53ac13fc19..d2b02a9173 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl @@ -55,18 +55,10 @@ bool LightIntersectsCluster(float3 position, float radiusSquared, ClusterAABB cl float radiusSquared = light.radius * light.radius; -#if defined(VR) - float3 positionVSLeft = FrameBuffer::WorldToView(light.positionWS[0].xyz, true, 0); - float3 positionVSRight = FrameBuffer::WorldToView(light.positionWS[1].xyz, true, 1); - - [branch] if (LightIntersectsCluster(positionVSLeft, radiusSquared, cluster) || LightIntersectsCluster(positionVSRight, radiusSquared, cluster)) - { -#else - float3 positionVS = FrameBuffer::WorldToView(light.positionWS[0].xyz, true, 0); + float3 positionVS = FrameBuffer::WorldToView(light.positionWS.xyz); [branch] if (LightIntersectsCluster(positionVS, radiusSquared, cluster)) { -#endif visibleLightIndices[visibleLightCount] = i; visibleLightCount++; if (visibleLightCount >= MAX_CLUSTER_LIGHTS) diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli index f2388d9ddd..38bd26679c 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli @@ -40,7 +40,7 @@ struct Light float invRadius; float fadeZone; float sizeBias; - float4 positionWS[2]; + float4 positionWS; uint4 roomFlags; uint lightFlags; uint shadowLightIndex; diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl index 46e34b175e..92e37d802c 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl @@ -6,7 +6,6 @@ #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" #include "Common/Random.hlsli" -#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcDepth : register(t0); @@ -98,14 +97,13 @@ float2x2 getRotationMatrix(float noise) const uint numSamples = 8; const float2 uv = (dtid + .5) * RCP_OUT_FRAME_DIM; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - const float2 screenPos = Stereo::ConvertFromStereoUV(uv, eyeIndex); + const float2 screenPos = uv; float depth = READ_DEPTH(srcDepth, dtid); - float3 pos = ScreenToViewPosition(screenPos, depth, eyeIndex); + float3 pos = ScreenToViewPosition(screenPos, depth); float3 normal = GBuffer::DecodeNormal(FULLRES_LOAD(srcNormalRoughness, dtid, uv, samplerLinearClamp).xy); - const float2 pixelDirRBViewspaceSizeAtCenterZ = depth.xx * (eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw) * RCP_OUT_FRAME_DIM; + const float2 pixelDirRBViewspaceSizeAtCenterZ = depth.xx * NDCToViewMul.xy * RCP_OUT_FRAME_DIM; const float worldRadius = radius * pixelDirRBViewspaceSizeAtCenterZ.x; float2x3 TvBv = getKernelBasis(normal, normal); // D = N float halfAngle = Math::HALF_PI; @@ -130,30 +128,18 @@ float2x2 getRotationMatrix(float noise) float2 poissonOffset = g_Poisson8[i].xy; - // Project viewspace blur offset to screen. In VR, if the sample leaves the - // current eye, try the other eye for cross-eye consistency at the seam. - // Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" float3 viewSamplePos = pos + TvBv[0] * poissonOffset.x + TvBv[1] * poissonOffset.y; - float2 screenPosSample = FrameBuffer::ViewToUV(viewSamplePos, true, eyeIndex); - uint sampleEyeIndex = eyeIndex; + float2 screenPosSample = FrameBuffer::ViewToUV(viewSamplePos); if (any(screenPosSample < 0) || any(screenPosSample > 1)) { -#ifdef VR - float3 worldSamplePos = FrameBuffer::ViewToWorld(viewSamplePos, true, eyeIndex); - screenPosSample = FrameBuffer::ViewToUV(FrameBuffer::WorldToView(worldSamplePos, true, 1 - eyeIndex), true, 1 - eyeIndex); - sampleEyeIndex = 1 - eyeIndex; - if (any(screenPosSample < 0) || any(screenPosSample > 1)) -#endif continue; } - float2 uvSample = Stereo::ConvertToStereoUV(screenPosSample, sampleEyeIndex); + float2 uvSample = screenPosSample; uvSample = (floor(uvSample * OUT_FRAME_DIM) + 0.5) * RCP_OUT_FRAME_DIM; // Snap to the pixel centre float depthSample = srcDepth.SampleLevel(samplerPointClamp, uvSample * frameScale, RES_MIP); - float3 posSample = ScreenToViewPosition(screenPosSample, depthSample, sampleEyeIndex); - if (sampleEyeIndex != eyeIndex) - posSample = FrameBuffer::WorldToView(FrameBuffer::ViewToWorld(posSample, true, sampleEyeIndex), true, eyeIndex); + float3 posSample = ScreenToViewPosition(screenPosSample, depthSample); float4 normalRoughnessSample = srcNormalRoughness.SampleLevel(samplerPointClamp, uvSample * frameScale, 0); float3 normalSample = GBuffer::DecodeNormal(normalRoughnessSample.xy); diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli b/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli index 8abea7831f..86bf0747c8 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli @@ -25,7 +25,7 @@ cbuffer SSGICB : register(b1) { - float4x4 PrevInvViewMat[2]; + float4x4 PrevInvViewMat; float4 NDCToViewMul; float4 NDCToViewAdd; @@ -86,8 +86,8 @@ float2 filterInf(float2 v) { return float2(filterInf(v.x), filterInf(v.y)); } float3 filterInf(float3 v) { return float3(filterInf(v.x), filterInf(v.y), filterInf(v.z)); } float4 filterInf(float4 v) { return float4(filterInf(v.x), filterInf(v.y), filterInf(v.z), filterInf(v.w)); } -// screenPos - normalised position in FrameDim, one eye only -// uv - normalised position in FrameDim, both eye +// screenPos - normalised position in FrameDim +// uv - normalised position in FrameDim // texCoord - texture coordinate #ifdef HALF_RES @@ -116,13 +116,10 @@ float4 filterInf(float4 v) { return float4(filterInf(v.x), filterInf(v.y), filte /////////////////////////////////////////////////////////////////////////////// // Inputs are screen XY and viewspace depth, output is viewspace position -float3 ScreenToViewPosition(const float2 screenPos, const float viewspaceDepth, const uint eyeIndex) +float3 ScreenToViewPosition(const float2 screenPos, const float viewspaceDepth) { - const float2 _mul = eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw; - const float2 _add = eyeIndex == 0 ? NDCToViewAdd.xy : NDCToViewAdd.zw; - float3 ret; - ret.xy = (_mul * screenPos.xy + _add) * viewspaceDepth; + ret.xy = (NDCToViewMul.xy * screenPos.xy + NDCToViewAdd.xy) * viewspaceDepth; ret.z = viewspaceDepth; return ret; } diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl index d04b4caa7b..0ad32a8ace 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl @@ -31,7 +31,6 @@ #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" -#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcWorkingDepth : register(t0); @@ -89,8 +88,7 @@ void CalculateGI( { const float2 frameScale = FrameDim * RcpTexDim; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - float2 normalizedScreenPos = Stereo::ConvertFromStereoUV(uv, eyeIndex); + float2 normalizedScreenPos = uv; const float rcpNumSlices = rcp((float)NumSlices); const float rcpNumSteps = rcp((float)NumSteps); @@ -98,7 +96,7 @@ void CalculateGI( // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance const float pixelTooCloseThreshold = 1.3; // approx viewspace pixel size at pixCoord; approximation of NDCToViewspace( uv.xy + ViewportSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; - const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * (eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw) * RCP_OUT_FRAME_DIM; + const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDCToViewMul.xy * RCP_OUT_FRAME_DIM; float screenspaceRadius = EffectRadius / pixelDirRBViewspaceSizeAtCenterZ.x; screenspaceRadius = max(MinScreenRadius, screenspaceRadius); @@ -107,8 +105,7 @@ void CalculateGI( ////////////////////////////////////////////////////////////////// - // Use mono screen-space position for noise indexing so both eyes - // sample the same noise for corresponding world positions. + // Use screen-space position for noise indexing. uint2 noiseCoord = uint2(normalizedScreenPos * OUT_FRAME_DIM); const float2 localNoise = SpatioTemporalNoise(noiseCoord, FrameIndex); const float noiseSlice = localNoise.x; @@ -116,7 +113,7 @@ void CalculateGI( ////////////////////////////////////////////////////////////////// - const float3 pixCenterPos = ScreenToViewPosition(normalizedScreenPos, viewspaceZ, eyeIndex); + const float3 pixCenterPos = ScreenToViewPosition(normalizedScreenPos, viewspaceZ); const float3 viewVec = normalize(-pixCenterPos); #ifdef GI_SPECULAR const float NoV = clamp(dot(viewVec, viewspaceNormal), 1e-5, 1); @@ -186,11 +183,7 @@ void CalculateGI( float2 samplePxCoord = dtid + .5 + sampleOffset * sideSign; float2 sampleUV = samplePxCoord * RCP_OUT_FRAME_DIM; - // Resolve which eye owns this sample. In VR, radial steps can cross the - // eye boundary in the side-by-side buffer; re-decode with the correct eye. - // Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" - uint sampleEyeIndex = Stereo::GetEyeIndexFromTexCoord(sampleUV); - float2 sampleScreenPos = Stereo::ConvertFromStereoUV(sampleUV, sampleEyeIndex); + float2 sampleScreenPos = sampleUV; [branch] if (any(sampleScreenPos > 1.0) || any(sampleScreenPos < 0.0)) continue; // Mip level grows with pixel-space distance from the centre. @@ -209,17 +202,9 @@ void CalculateGI( float SZ = srcWorkingDepth.SampleLevel(samplerPointClamp, sampleUV * frameScale, mipLevel); - // Reconstruct sample in current eye's viewspace for correct horizon angles. - float3 samplePos = ScreenToViewPosition(sampleScreenPos, SZ, sampleEyeIndex); - // For cross-eye samples, reject if the depth differs too much from the - // center pixel -- the other eye may see a different surface due to occlusion. -#if defined(VR) - if (sampleEyeIndex != eyeIndex) { - if (abs(SZ - viewspaceZ) > viewspaceZ * 0.1) - continue; - samplePos = FrameBuffer::WorldToView(FrameBuffer::ViewToWorld(samplePos, true, sampleEyeIndex), true, eyeIndex); - } -#endif + // Reconstruct sample viewspace position for correct horizon angles. + float3 samplePos = ScreenToViewPosition(sampleScreenPos, SZ); + // Reject if the depth differs too much from the center pixel. float3 sampleDelta = samplePos - pixCenterPos; float3 sampleHorizonVec = normalize(sampleDelta); @@ -276,7 +261,7 @@ void CalculateGI( frontBackMult = frontBackMult < 0 ? 0.0 : frontBackMult; // backface if (frontBackMult > 0.f) { - float3 sampleHorizonVecWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], half4(sampleHorizonVec, 0)).xyz); + float3 sampleHorizonVecWS = normalize(mul(FrameBuffer::CameraViewInverse, half4(sampleHorizonVec, 0)).xyz); float3 sampleRadiance = srcRadiance.SampleLevel(samplerPointClamp, sampleUV * OUT_FRAME_SCALE, mipLevelRadiance).rgb * frontBackMult * giBoost * countbits(validBits) * 0.03125; sampleRadiance = max(sampleRadiance, 0); @@ -347,14 +332,13 @@ void CalculateGI( uint2 pxCoord = dtid; float2 uv = (pxCoord + .5) * RCP_OUT_FRAME_DIM; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); float viewspaceZ = READ_DEPTH(srcWorkingDepth, pxCoord); float2 normalSample = FULLRES_LOAD(srcNormal, pxCoord, uv * OUT_FRAME_SCALE, samplerLinearClamp); float3 viewspaceNormal = GBuffer::DecodeNormal(normalSample); - half2 encodedWorldNormal = GBuffer::EncodeNormal(ViewToWorldVector(viewspaceNormal, FrameBuffer::CameraViewInverse[eyeIndex])); + half2 encodedWorldNormal = GBuffer::EncodeNormal(ViewToWorldVector(viewspaceNormal, FrameBuffer::CameraViewInverse)); outPrevGeo[pxCoord] = half3(viewspaceZ, encodedWorldNormal); // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl index 62ae96f1ec..442ce5a87d 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl @@ -27,10 +27,6 @@ RWTexture2D outDepth4 : register(u4); // is required to be non-linear (i.e. very large outdoors environments). float ClampDepth(float depth) { -#ifdef VR - if (depth == 0.0) // VR 0 indicates a mask - return 0.0; -#endif depth = ScreenToViewDepth(depth); return clamp(depth, 0.0, 3.402823466e+38); } diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl index d0fc087d63..f243bad154 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl @@ -2,7 +2,6 @@ #include "Common/FrameBuffer.hlsli" #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" -#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcDiffuse : register(t0); @@ -28,11 +27,11 @@ RWTexture2D outRemappedPrevGISpecular : register(u5); #endif void readHistory( - uint eyeIndex, float curr_depth, float3 curr_pos, int2 pixCoord, float bilinear_weight, + float curr_depth, float3 curr_pos, int2 pixCoord, float bilinear_weight, inout half prev_ao, inout half4 prev_y, inout half2 prev_co_cg, inout half3 prev_ambient, inout float accum_frames, inout half4 prev_gi_specular, inout float wsum) { const float2 uv = (pixCoord + .5) * RCP_OUT_FRAME_DIM; - const float2 screen_pos = Stereo::ConvertFromStereoUV(uv, eyeIndex); + const float2 screen_pos = uv; if (any(screen_pos < 0) || any(screen_pos > 1)) return; @@ -43,12 +42,12 @@ void readHistory( // Early reject: skip bilinear taps on a different surface before the // expensive world-space reconstruction. Use a wider threshold than the // world-space check to avoid rejecting valid taps displaced by parallax - // (e.g. VR head rotation). + // (e.g. camera rotation). if (abs(curr_depth - prev_depth) > curr_depth * DepthDisocclusion * 3) return; - float3 prev_pos = ScreenToViewPosition(screen_pos, prev_depth, eyeIndex); - prev_pos = ViewToWorldPosition(prev_pos, PrevInvViewMat[eyeIndex]) + FrameBuffer::CameraPreviousPosAdjust[eyeIndex].xyz; + float3 prev_pos = ScreenToViewPosition(screen_pos, prev_depth); + prev_pos = ViewToWorldPosition(prev_pos, PrevInvViewMat) + FrameBuffer::CameraPreviousPosAdjust.xyz; float3 delta_pos = curr_pos - prev_pos; // float normal_prod = dot(curr_normal, prev_normal); @@ -75,14 +74,13 @@ void readHistory( const float2 frameScale = FrameDim * RcpTexDim; const float2 uv = (pixCoord + .5) * RCP_OUT_FRAME_DIM; - const uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - const float2 screen_pos = Stereo::ConvertFromStereoUV(uv, eyeIndex); + const float2 screen_pos = uv; float2 prev_screen_pos = screen_pos; #ifdef REPROJECTION prev_screen_pos += FULLRES_LOAD(srcMotionVec, pixCoord, uv * frameScale, samplerLinearClamp).xy; #endif - float2 prev_uv = Stereo::ConvertToStereoUV(prev_screen_pos, eyeIndex); + float2 prev_uv = prev_screen_pos; half3 prev_ambient = 0; half prev_ao = 0; @@ -105,24 +103,24 @@ void readHistory( #ifdef REPROJECTION if ((curr_depth <= DepthFadeRange.y) && !(any(prev_screen_pos < 0) || any(prev_screen_pos > 1))) { // float3 curr_normal = GBuffer::DecodeNormal(srcCurrNormal[pixCoord]); - // curr_normal = ViewToWorldVector(curr_normal, FrameBuffer::CameraViewInverse[eyeIndex]); - float3 curr_pos = ScreenToViewPosition(screen_pos, curr_depth, eyeIndex); - curr_pos = ViewToWorldPosition(curr_pos, FrameBuffer::CameraViewInverse[eyeIndex]) + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + // curr_normal = ViewToWorldVector(curr_normal, FrameBuffer::CameraViewInverse); + float3 curr_pos = ScreenToViewPosition(screen_pos, curr_depth); + curr_pos = ViewToWorldPosition(curr_pos, FrameBuffer::CameraViewInverse) + FrameBuffer::CameraPosAdjust.xyz; float2 prev_px_coord = prev_uv * OUT_FRAME_DIM; int2 prev_px_lu = floor(prev_px_coord - 0.5); float2 bilinear_weights = prev_px_coord - 0.5 - prev_px_lu; - readHistory(eyeIndex, curr_depth, curr_pos, + readHistory(curr_depth, curr_pos, prev_px_lu, (1 - bilinear_weights.x) * (1 - bilinear_weights.y), prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(eyeIndex, curr_depth, curr_pos, + readHistory(curr_depth, curr_pos, prev_px_lu + int2(1, 0), bilinear_weights.x * (1 - bilinear_weights.y), prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(eyeIndex, curr_depth, curr_pos, + readHistory(curr_depth, curr_pos, prev_px_lu + int2(0, 1), (1 - bilinear_weights.x) * bilinear_weights.y, prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(eyeIndex, curr_depth, curr_pos, + readHistory(curr_depth, curr_pos, prev_px_lu + int2(1, 1), bilinear_weights.x * bilinear_weights.y, prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl deleted file mode 100644 index 365e50236f..0000000000 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl +++ /dev/null @@ -1,122 +0,0 @@ -// Stereo Sync - Bilateral blend of SSGI buffers between eyes -// -// Reprojects each pixel to the other eye and blends AO/IL based on depth -// agreement. Runs after the SSGI blur to reduce per-eye GI disparities. -// -// Based on: Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space -// ambient occlusion" https://eprints.whiterose.ac.uk/id/eprint/187713/ - -#include "Common/FrameBuffer.hlsli" -#include "Common/VR.hlsli" -#include "ScreenSpaceGI/common.hlsli" - -#ifdef VR - -Texture2D srcDepth : register(t0); -Texture2D srcAo : register(t1); -Texture2D srcIlY : register(t2); -Texture2D srcIlCoCg : register(t3); - -RWTexture2D outAo : register(u0); -RWTexture2D outIlY : register(u1); -RWTexture2D outIlCoCg : register(u2); - -static const float kDepthSigma = 0.01; // Bilateral depth tolerance (NDC): surfaces within this range are considered the same and blended -static const float kMaxBlend = 0.5; // Maximum stereo blend weight; 0.5 gives equal weighting between eyes -static const float kEdgeRelThreshold = 0.5; // Relative linear-depth difference above which a pixel is a depth discontinuity (50% change) -static const float kMaskDepth = 0.01; // Linear depth sentinel: values below this are outside the HMD lens area -static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check - -// Writes all output channels from the source buffers (passthrough / no-blend path). -void Passthrough(uint2 dtid) -{ - outAo[dtid] = srcAo[dtid]; - outIlY[dtid] = srcIlY[dtid]; - outIlCoCg[dtid] = srcIlCoCg[dtid]; -} - -// Samples four depth neighbors in a cross pattern (±step.x, ±step.y) around centerUV, -// scaled by texScale to map from output UV space to texture sample coords. -// centerUV is clamped to eyeIndex's half of the stereo buffer before offsetting -// to prevent neighbor reads from crossing the x=0.5 seam into the other eye. -float4 SampleCrossDepths(float2 centerUV, float2 step, float2 texScale, uint eyeIndex) -{ - float2 uv = Stereo::ClampToEyeUV(centerUV, eyeIndex); - return float4( - srcDepth.SampleLevel(samplerPointClamp, (uv + float2(step.x, 0)) * texScale, RES_MIP), - srcDepth.SampleLevel(samplerPointClamp, (uv + float2(-step.x, 0)) * texScale, RES_MIP), - srcDepth.SampleLevel(samplerPointClamp, (uv + float2(0, step.y)) * texScale, RES_MIP), - srcDepth.SampleLevel(samplerPointClamp, (uv + float2(0, -step.y)) * texScale, RES_MIP)); -} - -[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { - const float2 outFrameDim = OUT_FRAME_DIM; - if (any(dtid >= uint2(outFrameDim))) - return; - - const float2 frameScale = FrameDim * RcpTexDim; - float2 uv = (dtid + 0.5) / outFrameDim; - - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - // SSGI working depth is linear view-space Z. - // 0.0 = mask (outside lens area). FP_Z = first-person hands threshold (~18.0). - float depth = srcDepth.SampleLevel(samplerPointClamp, uv * frameScale, RES_MIP); - if (depth < FP_Z) { - Passthrough(dtid); - return; - } - - // Source edge detection: skip stereo sync at depth discontinuities. - // Uses a relative threshold since depth is linear view-space (not NDC). - // Placed before rawDepth conversion and reprojection to save VP matrix work - // for edge pixels. - float2 pixelStep = 1.0 / outFrameDim; - float4 srcNeighborDepths = SampleCrossDepths(uv, pixelStep, frameScale, eyeIndex); - if (Stereo::MaxDepthDiff(depth, srcNeighborDepths) / max(depth, 1.0) > kEdgeRelThreshold) { - Passthrough(dtid); - return; - } - - // Convert linear depth to raw depth (NDC Z) for reprojection matrix math. - // raw = (CameraData.x - CameraData.w / depth) / CameraData.z - // where x=n*f, w=f, z=f-n - float rawDepth = (SharedData::CameraData.x - SharedData::CameraData.w / depth) / SharedData::CameraData.z; - - Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, rawDepth, eyeIndex, outFrameDim); - - if (!r.valid) { - Passthrough(dtid); - return; - } - - float otherLinearDepth = srcDepth.SampleLevel(samplerPointClamp, r.otherStereoUV * frameScale, RES_MIP); - if (otherLinearDepth < FP_Z) { - Passthrough(dtid); - return; - } - - // Destination edge detection: skip if the reprojected pixel is near the HMD mask - // boundary or at a depth discontinuity in the other eye. Due to VR parallax the - // arm silhouette appears at a different screen position per eye, so the reprojection - // can cross a boundary invisible from this eye's perspective. - float2 marginStep = float(kEdgeMargin) / outFrameDim; - float4 otherNeighborDepths = SampleCrossDepths(r.otherStereoUV, marginStep, frameScale, 1 - eyeIndex); - if (any(otherNeighborDepths < kMaskDepth) || - Stereo::MaxDepthDiff(otherLinearDepth, otherNeighborDepths) / max(otherLinearDepth, 1.0) > kEdgeRelThreshold) { - Passthrough(dtid); - return; - } - - float otherRawDepth = (SharedData::CameraData.x - SharedData::CameraData.w / otherLinearDepth) / SharedData::CameraData.z; - - // Back-check disabled: source + destination edge detection covers the occlusion - // boundary cases it was guarding, saving 2 VP matrix multiplies per blended pixel. - Stereo::FinalizeStereoBlend(r, uv, rawDepth, otherRawDepth, eyeIndex, outFrameDim, kDepthSigma, kMaxBlend, 0.0); - - outAo[dtid] = lerp(srcAo[dtid], srcAo[r.otherPx], r.blendWeight); - outIlY[dtid] = lerp(srcIlY[dtid], srcIlY[r.otherPx], r.blendWeight); - outIlCoCg[dtid] = lerp(srcIlCoCg[dtid], srcIlCoCg[r.otherPx], r.blendWeight); -} - -#endif // VR diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl index 17078de682..62971b5383 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl @@ -56,12 +56,7 @@ cbuffer PerFrame : register(b1) parameters.DynamicRes = DynamicRes; -#if defined(VR) - // Disabled in VR: depth bias causes subtle shadow shifting at stereo seams on camera motion. - parameters.UsePrecisionOffset = false; -#else parameters.UsePrecisionOffset = true; -#endif WriteScreenSpaceShadow(parameters, groupID, groupThreadID); } \ No newline at end of file diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli index a1a6929c33..9f94e4c708 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli @@ -3,7 +3,7 @@ namespace ScreenSpaceShadows { Texture2D ScreenSpaceShadowsTexture : register(t45); - float GetScreenSpaceShadow(float3 screenPosition, float2 uv, float noise, uint eyeIndex) + float GetScreenSpaceShadow(float3 screenPosition, float2 uv, float noise) { return ScreenSpaceShadowsTexture.Load(int3(int2(screenPosition.xy + 0.5f), 0)).x; } diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl deleted file mode 100644 index 47e88e9f90..0000000000 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl +++ /dev/null @@ -1,169 +0,0 @@ -// Stereo Sync + Blur - Combined bilateral stereo blend and depth-weighted -// blur for VR screen-space shadows. Runs as a single compute pass after the -// raymarch to both synchronize shadow data between eyes and smooth per-pixel -// noise. -// -// Based on: Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space -// ambient occlusion" https://eprints.whiterose.ac.uk/id/eprint/187713/ - -#include "Common/FrameBuffer.hlsli" -#include "Common/Math.hlsli" -#include "Common/Random.hlsli" -#include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" - -#ifdef VR - -// Match the C++ depth binding format for strict typing. -// TERRAIN_BLENDING ON -> R32_FLOAT (no unorm). OFF -> R24_UNORM_X8_TYPELESS (unorm). -# if defined(TERRAIN_BLENDING) -Texture2D SrcDepthTexture : register(t0); -# else -Texture2D SrcDepthTexture : register(t0); -# endif -Texture2D SrcShadowTexture : register(t1); - -RWTexture2D OutShadowTexture : register(u0); - -cbuffer StereoSyncCB : register(b1) -{ - float2 FrameDim; - float2 RcpFrameDim; -}; - -static const float kDepthSigma = 0.01; // Bilateral depth tolerance (NDC): surfaces within this range are considered the same and blended -static const float kMaxBlend = 1.0; // Maximum stereo blend weight; reduce below 1.0 to soften the cross-eye contribution -static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo sync -static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check - -// Depth-weighted 4-sample blur using a rotated Poisson disk. -// Uses dtid hash for per-pixel rotation to break structured patterns. -float BlurShadow(int2 dtid, float centerDepth) -{ - // Per-pixel rotation from interleaved gradient noise - float noise = Random::InterleavedGradientNoise(float2(dtid)); - float angle = noise * Math::TAU; - float sn, cs; - sincos(angle, sn, cs); - float2x2 rot = float2x2(cs, sn, -sn, cs); - - static const float2 kOffsets[4] = { - float2(0.382, 0.892), - float2(0.491, 0.217), - float2(0.938, 0.735), - float2(0.009, 0.056), - }; - - float weight = 0; - float shadow = 0; - - [unroll] for (uint i = 0; i < 4; i++) - { - float2 offset = mul(kOffsets[i], rot); - int2 samplePx = dtid + int2(offset * 2.5); - samplePx = clamp(samplePx, int2(0, 0), int2(FrameDim) - 1); - - float sampleDepth = SrcDepthTexture[samplePx]; - - if (sampleDepth < 1e-5) - continue; - - float attenuation = 1.0 - saturate(100.0 * abs(sampleDepth - centerDepth) / max(centerDepth, 1e-5)); - - if (attenuation > 0.0) { - shadow += SrcShadowTexture[samplePx] * attenuation; - weight += attenuation; - } - } - - return weight > 0.0 ? shadow / weight : SrcShadowTexture[dtid]; -} - -// Samples four depth neighbors in a cross pattern (±offset pixels) around center, -// clamped to eyeIndex's half of the packed stereo buffer to avoid seam contamination. -float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) -{ - return float4( - SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(offset, 0), eyeIndex, FrameDim)], - SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(-offset, 0), eyeIndex, FrameDim)], - SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(0, offset), eyeIndex, FrameDim)], - SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(0, -offset), eyeIndex, FrameDim)]); -} - -[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { - if (any(dtid >= uint2(FrameDim))) - return; - - float2 uv = (dtid + 0.5) * RcpFrameDim; - - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - float depth = SrcDepthTexture[dtid]; - - // depth == 0: VR HMD mask; depth == 1: sky/far plane - if (depth < 1e-5 || depth >= 1.0) { - OutShadowTexture[dtid] = SrcShadowTexture[dtid]; - return; - } - - // Skip stereo sync for first-person geometry interior (hands/weapons). - // Placed before the blur: arm shadow is uniform so the bilateral blur - // would return SrcShadowTexture[dtid] unchanged anyway. - float linearDepth = SharedData::GetScreenDepth(depth); - if (linearDepth < VR_FP_Z) { - OutShadowTexture[dtid] = SrcShadowTexture[dtid]; - return; - } - - // Skip stereo sync at depth discontinuities (arm/world silhouettes, object edges). - // Placed before the blur: the bilateral depth weighting zeroes out cross-edge - // samples, so the blur collapses to SrcShadowTexture[dtid] at these pixels anyway. - float4 edgeDepths = SampleCrossDepths(dtid, 1, eyeIndex); - if (Stereo::MaxDepthDiff(depth, edgeDepths) > kEdgeDepthThreshold) { - OutShadowTexture[dtid] = SrcShadowTexture[dtid]; - return; - } - - // Depth-weighted blur on this eye's shadow data. - // Only reached by world pixels that will attempt stereo sync. - float myShadow = BlurShadow(dtid, depth); - - Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, depth, eyeIndex, FrameDim); - - if (!r.valid) { - OutShadowTexture[dtid] = myShadow; - return; - } - - float otherDepth = SrcDepthTexture[r.otherPx]; - - // Skip if other eye sees mask, sky, or first-person geometry - if (otherDepth < 1e-5 || otherDepth >= 1.0 || SharedData::GetScreenDepth(otherDepth) < VR_FP_Z) { - OutShadowTexture[dtid] = myShadow; - return; - } - - // Reject if reprojected pixel is near the HMD mask boundary, or if it sits - // at a depth discontinuity in the other eye. The source-side edge check above - // only fires when *this* eye sees the boundary; due to VR parallax the arm - // silhouette appears at a different screen position in each eye, so the - // reprojection can cross a boundary invisible from this eye's perspective. - // Reusing the same four neighbor reads covers both purposes at no extra cost. - float4 otherNeighbors = SampleCrossDepths(r.otherPx, kEdgeMargin, 1 - eyeIndex); - if (any(otherNeighbors < 1e-5) || Stereo::MaxDepthDiff(otherDepth, otherNeighbors) > kEdgeDepthThreshold) { - OutShadowTexture[dtid] = myShadow; - return; - } - - // Source + destination edge detection - Stereo::FinalizeStereoBlend(r, uv, depth, otherDepth, eyeIndex, FrameDim, kDepthSigma, kMaxBlend, 0.0); - - float otherShadow = SrcShadowTexture[r.otherPx]; - - // Use min (darkest) when depths agree: if either eye detected an - // occluder, that shadow should be visible. - float combined = min(myShadow, otherShadow); - OutShadowTexture[dtid] = lerp(myShadow, combined, r.blendWeight); -} - -#endif // VR diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli index 832f578fc3..5ca905514a 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli @@ -253,35 +253,8 @@ void WriteScreenSpaceShadow(DispatchParameters inParameters, int3 inGroupID, int half2 coord = read_xy * inParameters.InvDepthTextureSize * inParameters.DynamicRes; half2 coord_with_offset = (read_xy + offset_xy) * inParameters.InvDepthTextureSize * inParameters.DynamicRes; -#if defined(VR) - // VR side-by-side: halve x to map stereo pixel coords to texture UV. - coord *= half2(0.5, 1.0); - coord_with_offset *= half2(0.5, 1.0); - -# if defined(RIGHT) - // Right eye: valid UV range is [0.5*DynRes.x, DynRes.x] - bool coord_out_of_eye = coord.x < 0.5 * inParameters.DynamicRes.x; - bool coord_offset_out_of_eye = coord_with_offset.x < 0.5 * inParameters.DynamicRes.x; -# else - // Left eye: valid UV range is [0.0, 0.5*DynRes.x) - bool coord_out_of_eye = coord.x >= 0.5 * inParameters.DynamicRes.x; - bool coord_offset_out_of_eye = coord_with_offset.x >= 0.5 * inParameters.DynamicRes.x; -# endif - - // Clamp cross-eye depth reads to FarDepthValue (1.0) so rays near the SBS center - // seam see no occluder at the boundary. Shadow weakens by ~1 pixel at the seam but - // stays temporally stable across camera movement. - depths.x = coord_out_of_eye ? 1.0 : inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord, 0); - depths.y = coord_offset_out_of_eye ? 1.0 : inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord_with_offset, 0); - - // HMD mask: depth==0 is outside the visible lens area. Remap to FarDepthValue so - // mask pixels do not cast false shadows. - depths.x = lerp(depths.x, 1.0, (float)(depths.x == 0)); // Stencil area - depths.y = lerp(depths.y, 1.0, (float)(depths.y == 0)); // Stencil area -#else depths.x = inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord, 0); depths.y = inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord_with_offset, 0); -#endif // Depth thresholds (bilinear/shadow thickness) are based on a fractional ratio of the difference between sampled depth and the far clip depth static const half kDepthThicknessFloor = 1e-4h; // Prevents division by zero in depth_scale when depth is at the far clip plane @@ -332,19 +305,6 @@ void WriteScreenSpaceShadow(DispatchParameters inParameters, int3 inGroupID, int // Sync wavefronts now groupshared DepthData is written GroupMemoryBarrierWithGroupSync(); -#if defined(VR) - // Check if the pixel we're writing to is on the correct eye side - half writeX = write_xy.x * inParameters.InvDepthTextureSize.x; - -# if defined(RIGHT) - if (writeX < 0.0) - return; -# else - if (writeX > 1.0) - return; -# endif -#endif - half start_depth = sampling_depth[0]; if (start_depth == 0.0 || start_depth == 1.0) diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index 6fb969ff35..39281513cb 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -48,7 +48,7 @@ float3 GetScalingFactor(float3 albedo) return 3.5f + 100.f * pow(abs(value), 4); } -float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssAmount, bool humanProfile) +float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, float sssAmount, bool humanProfile) { float centerDepth = SharedData::GetScreenDepth(DepthTexture[DTid].x); @@ -71,7 +71,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA float3 d3d = diffuseMeanFreePath.xyz * dmfpForSampling / s3d; const float3 normalVS = GBuffer::DecodeNormal(NormalTexture[DTid].xy); - const float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); + const float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(normalVS, 0)).xyz); float3 weightSum = 0.0f; float3 colorSum = 0.0f; @@ -115,7 +115,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssA float3 sampleColor = ColorTexture[samplePixcoord].xyz * maskSample; float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x); float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy); - float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz); + float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(sampleNormalVS, 0)).xyz); float deltaDepth = (sampleDepth - centerDepth) * 10.f / GAME_UNIT_TO_CM; // convert to mm float radiusSampledInMM = sqrt(radius * radius + deltaDepth * deltaDepth); diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl index e6457a05af..7b595fc74d 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl @@ -25,7 +25,6 @@ SamplerState PointSampler : register(s0); return; float2 texCoord = (DTid.xy + 0.5) * SharedData::BufferDim.zw; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(texCoord); #if defined(BURLEY) @@ -34,7 +33,7 @@ SamplerState PointSampler : register(s0); if (sssAmount > 0.0) { bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); + float4 color = BurleyNormalizedSS(DTid.xy, texCoord, sssAmount, humanProfile); SSSRW[DTid.xy] = max(0, color); } diff --git a/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl b/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl deleted file mode 100644 index df107d9175..0000000000 --- a/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl +++ /dev/null @@ -1,23 +0,0 @@ -// Zeros color in the HMD hidden area per eye. -// Prevents DLSS/FSR from temporally accumulating the engine's sky/ambient clear color -// into visible pixels during head movement ("light blue border" ghosting). -// depth == 0.0 is the unrendered/hidden area value (Skyrim reversed-Z: far plane = 0). -// DepthIn is the combined stereo depth buffer; DepthOffsetX selects the eye's half. -// ColorInOut is the isolated per-eye buffer; ColorOffsetX is always 0. - -cbuffer ClearHMDMaskCB : register(b0) -{ - uint DepthOffsetX; // X offset into combined stereo depth (0 = left, eyeWidth = right) - uint ColorOffsetX; // X offset into color target (always 0 for per-eye buffers) - uint pad0; - uint pad1; -}; - -Texture2D DepthIn : register(t0); -RWTexture2D ColorInOut : register(u0); - -[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { - // Read from stereo depth, write to potentially stereo color - if (DepthIn[dispatchID.xy + uint2(DepthOffsetX, 0)] == 0.0) - ColorInOut[dispatchID.xy + uint2(ColorOffsetX, 0)] = float4(0.0, 0.0, 0.0, 0.0); -} diff --git a/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl index 2788b913cf..1a88fd3530 100644 --- a/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl @@ -18,10 +18,6 @@ SamplerState LinearSampler : register(s0); Texture2D RefractionNormals : register(t0); Texture2D DepthTex : register(t1); -# if defined(VR) -Texture2D StencilTex : register(t2); -# endif - cbuffer JitterCB : register(b0) { float2 jitter; @@ -59,36 +55,14 @@ PS_OUTPUT main(PS_INPUT input) // Remove jitter offset to get the correct sampling coordinates float2 uv = originalUV - (jitter * SharedData::BufferDim.zw); - // Clamp within dynamic-resolution bounds (VR: preserve per-eye bounds). + // Clamp within dynamic-resolution bounds. uv = FrameBuffer::ClampDynamicResolutionAdjustedScreenPosition(uv, input.TexCoord); -# if defined(VR) - uint4 stencilSamples = StencilTex.GatherRed(LinearSampler, uv); - - // Choose the minimum stencil value - uint minStencil = min(min(stencilSamples.x, stencilSamples.y), min(stencilSamples.z, stencilSamples.w)); - - // Only write depth/stencil that is inside the viewable area - if (minStencil > 0x00) - discard; -# endif - // Upscale using linear sampling psout.RefractionNormals = RefractionNormals.SampleLevel(LinearSampler, uv, 0); psout.Depth = DepthTex.SampleLevel(LinearSampler, uv, 0); -# if defined(VR) - float bilinearDepth = psout.Depth; - if (useWideKernel > 0.5f) { - psout.Depth = SampleMinDepthWideGather(uv); - } else { - psout.Depth = SampleMinDepth2x2(uv); - } - // Keep SAO camera Z smooth to avoid over-occlusion; depth culling uses SV_Depth. - psout.SAOCameraZ = bilinearDepth; -# else psout.SAOCameraZ = psout.Depth; -# endif return psout; } diff --git a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl index 34ea8250a8..94d48eb554 100644 --- a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl @@ -2,9 +2,8 @@ cbuffer UpscalingData : register(b0) { - float2 TrueSamplingDim; // per-eye render dim in VR, full render dim otherwise - uint EyeOffsetX; // X offset into stereo source buffers; 0 for non-VR / left eye - uint pad0; + float2 TrueSamplingDim; + float2 pad0; }; Texture2D TAAMask : register(t0); @@ -20,22 +19,19 @@ RWTexture2D DepthOutput : register(u3); #endif [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { - // Bounds check in per-eye space; EyeOffsetX=0 makes this identical to the old path for non-VR + // Bounds check if (any(dispatchID.xy >= uint2(TrueSamplingDim))) return; - // All source reads are in full stereo space; outputs are 0-based (per-eye or full-frame) - uint2 srcCoord = dispatchID.xy + uint2(EyeOffsetX, 0); - - float2 taaMask = TAAMask[srcCoord]; - float transparencyCompositionMask = NormalsWaterMask[srcCoord].z; + float2 taaMask = TAAMask[dispatchID.xy]; + float transparencyCompositionMask = NormalsWaterMask[dispatchID.xy].z; #if defined(DLSS) - float depth = DepthMask[srcCoord]; + float depth = DepthMask[dispatchID.xy]; float nearFactor = smoothstep(4096.0 * 2.5, 0.0, SharedData::GetScreenDepth(depth)); // Find longest motion vector in 5x5 neighborhood - float2 motionVector = MotionVectorMask[srcCoord]; + float2 motionVector = MotionVectorMask[dispatchID.xy]; float2 longestMotionVector = motionVector; float maxMotionLengthSq = dot(motionVector, motionVector); @@ -45,18 +41,15 @@ RWTexture2D DepthOutput : register(u3); { int2 samplePos = int2(dispatchID.xy) + int2(x, y); - // Bounds check stays in per-eye space — prevents cross-eye contamination in VR - // and out-of-bounds reads in non-VR (EyeOffsetX=0 makes these equivalent) + // Bounds check if (any(samplePos < 0) || any(samplePos >= int2(TrueSamplingDim))) continue; - // Source read uses full stereo offset - int2 srcPos = samplePos + int2(EyeOffsetX, 0); - float neighborDepth = DepthMask[srcPos]; + float neighborDepth = DepthMask[samplePos]; // Take neighbor if it's longer AND closer if (neighborDepth < depth) { - float2 neighborMotionVector = MotionVectorMask[srcPos]; + float2 neighborMotionVector = MotionVectorMask[samplePos]; // Square motion vector for length float motionLengthSq = dot(neighborMotionVector, neighborMotionVector); @@ -74,12 +67,12 @@ RWTexture2D DepthOutput : register(u3); #if defined(DEPTH_OUTPUT) // Copy depth as R32_FLOAT so FSR DX11 backend receives a typed format. - // The raw depth resource is R24G8_TYPELESS in VR which maps to FFX_SURFACE_FORMAT_UNKNOWN. - DepthOutput[dispatchID.xy] = DepthMask[srcCoord]; + // The raw depth resource is R24G8_TYPELESS which maps to FFX_SURFACE_FORMAT_UNKNOWN. + DepthOutput[dispatchID.xy] = DepthMask[dispatchID.xy]; #endif float reactiveMask = taaMask.x * 0.1 + taaMask.y; ReactiveMask[dispatchID.xy] = reactiveMask; TransparencyCompositionMask[dispatchID.xy] = transparencyCompositionMask; -} \ No newline at end of file +} diff --git a/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl index 213ccd6931..37386b8438 100644 --- a/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl @@ -15,9 +15,6 @@ struct PS_OUTPUT SamplerState LinearSampler : register(s0); Texture2D UnderwaterMask : register(t0); -# if defined(VR) -Texture2D SceneDepth : register(t1); -# endif cbuffer JitterCB : register(b0) { @@ -38,95 +35,6 @@ PS_OUTPUT main(PS_INPUT input) // Clamp within bounds uv = clamp(uv, 0.0, FrameBuffer::DynamicResolutionParams1.xy); -# if defined(VR) - // In VR the vanilla waterline draw (DrawIndexedInstanced, 2 instances) emits - // identical left-eye clip positions for both instances. The internal-res mask - // therefore only represents the left eye: the right-eye half of the buffer - // contains the tapered apex of the left-eye polygon, which is nearly all black. - // GetDynamicResolutionAdjustedScreenPosition then samples that black region for - // the right eye, making the entire right-eye underwater fog incorrect. - // - // Fix: reconstruct the mask analytically per-eye. For a horizontal water plane - // at height waterHeight, a pixel is "underwater" (mask = 1) when: - // - the camera itself is below the water surface, OR - // - the ray from the per-eye camera through this pixel points downward - // (rayDir.z < 0), meaning it looks below the water plane. - // This exactly reproduces what the vanilla waterline polygon approximates, - // but correctly per-eye. - - uint eyeIndex = (input.TexCoord.x >= 0.5) ? 1 : 0; - - // WaterData is a 5×5 grid centered on the camera; tile 12 (row 2, col 2) is - // always the camera's own tile. Pass eyeIndex so GetWaterData corrects the .w - // (water surface height) from eye-0 camera-relative Z into the current eye's frame. - // GetWaterData expects a camera-relative XY position; float3(0,0,0) is the camera - // itself, which always maps to the center tile (12). - float waterHeight = SharedData::GetWaterData(float3(0, 0, 0), eyeIndex).w; - - // Tile sentinel: try TESWaterSystem fallback. WaterSystemHeight is valid only when - // playerUnderwater == true (fully submerged); it is stored eye-0 camera-relative so - // the same per-eye correction as GetWaterData applies. - if (waterHeight <= WATER_HEIGHT_NO_TILE_SENTINEL) { - float sysHeight = SharedData::WaterSystemHeight; - if (sysHeight > WATER_HEIGHT_NO_TILE_SENTINEL) - waterHeight = sysHeight + FrameBuffer::CameraPosAdjust[0].z - FrameBuffer::CameraPosAdjust[eyeIndex].z; - } - - // GetWaterData returns INT_MIN (~-2.147e9) when the tile is outside the 5x5 grid. - if (waterHeight > WATER_HEIGHT_NO_TILE_SENTINEL) { - // Unpack from side-by-side stereo layout to per-eye UV [0, 1] - float2 eyeUV = float2(input.TexCoord.x * 2.0 - (float)eyeIndex, input.TexCoord.y); - - // Convert to NDC [-1, 1]. UV y=0 is the top of the screen; NDC y=+1 is the top. - float2 ndc = float2(eyeUV.x * 2.0 - 1.0, 1.0 - eyeUV.y * 2.0); - - // Sample depth using the shared de-jittered stereo UV (already DR-adjusted above). - // uv is in stereo space so no ConvertUVToSampleCoord round-trip is needed. - float depth = SceneDepth.Load(int3(uv * SharedData::BufferDim.xy, 0)).x; - - if (depth > EPSILON_DEPTH_SKY) { - // Geometry pixel: reconstruct world position from depth. - // CameraViewProjInverse[eyeIndex] maps clip-space back to the per-eye - // camera-relative world space. waterHeight has been adjusted to the same - // frame, so the comparison is correct for both eyes. - float4 worldPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4(ndc, depth, 1.0)); - worldPos /= worldPos.w; - // kSurfaceBias (Skyrim world units, ~1 unit ≈ 1.4 cm) anchors the mask - // threshold relative to the flat waterHeight plane to absorb wave-vertex - // displacement (measured max trough ≈ 2.92 units; 3.5 gives margin). - // - // The threshold direction depends on view orientation: - // Looking UP (worldPos.z > 0, pixel above camera in world space): - // Camera is below the surface viewing it from underneath. - // Expand threshold upward by +kSurfaceBias so the entire wave surface - // (crests and troughs alike) is included in the masked region. - // Looking DOWN (worldPos.z <= 0, pixel below or level with camera): - // The surface is seen from above or the camera is above water. - // Shrink threshold downward by -kSurfaceBias so the surface itself - // is excluded from the mask (no fog on the surface seen from above). - static const float kSurfaceBias = 3.5; - bool lookingUp = worldPos.z > 0.0; - bool cameraUnderwater = waterHeight > 0.0; - float threshold = (cameraUnderwater && lookingUp) ? waterHeight + kSurfaceBias : waterHeight - kSurfaceBias; - psout.UnderwaterMask = (worldPos.z < threshold) ? 1.0 : 0.0; - } else { - // depth <= EPSILON_DEPTH_SKY: sky / unrendered pixels (reversed-Z depth clear value). - // Unproject to obtain the per-pixel ray direction and decide based on that. - float4 worldFarPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4(ndc, 0.0, 1.0)); - worldFarPos /= worldFarPos.w; - float3 rayDir = normalize(worldFarPos.xyz); - // Per-eye waterHeight > 0 means the water surface is above THIS eye's camera - // (eye is below water); <= 0 means the eye camera is above the water surface. - psout.UnderwaterMask = (waterHeight > 0.0 || rayDir.z < 0.0) ? 1.0 : 0.0; - } - return psout; - } - // No water tile or system height available: fall through to the standard sampler path. - // The left-eye result from the vanilla mask is still accurate here; the right-eye - // will be approximate, but both sources failing implies no nearby water so the - // visual impact is nil. -# endif - // Upscale using linear sampling with jitter-corrected coordinates psout.UnderwaterMask = UnderwaterMask.SampleLevel(LinearSampler, uv, 0); diff --git a/features/VR/CORE b/features/VR/CORE deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/VR/Shaders/Features/VR.ini b/features/VR/Shaders/Features/VR.ini deleted file mode 100644 index 5dd39c9cbd..0000000000 --- a/features/VR/Shaders/Features/VR.ini +++ /dev/null @@ -1,2 +0,0 @@ -[Info] -Version = 1-1-0 diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli index cdfb339ba2..bf703beedf 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli @@ -57,17 +57,17 @@ namespace VolumetricShadows return shadow * rcpSampleCount; } - float GetVSMShadow3D(float3 startPosition, float3 endPosition, float noise, uint baseSampleCount, uint eyeIndex, out float surfaceShadow) + float GetVSMShadow3D(float3 startPosition, float3 endPosition, float noise, uint baseSampleCount, out float surfaceShadow) { DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; // View-space z — matches the linear cascade split distances from BSShadowDirectionalLight. float3 midPosition = (startPosition + endPosition) * 0.5; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(midPosition, eyeIndex)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(midPosition)); // Cascade projections are world-space; positions come in camera-relative. - startPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; - endPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + startPosition += FrameBuffer::CameraPosAdjust.xyz; + endPosition += FrameBuffer::CameraPosAdjust.xyz; // Early out beyond cascade range if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) { @@ -130,11 +130,11 @@ namespace VolumetricShadows return ComputeVSM(moments, positionLS.z); } - float GetVSMShadow2D(float3 position, uint eyeIndex, out float detailedShadow) + float GetVSMShadow2D(float3 position, out float detailedShadow) { DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(position, eyeIndex)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(position)); // Early out beyond cascade range if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) { @@ -146,7 +146,7 @@ namespace VolumetricShadows float fade = saturate(shadowMapDepth / directionalShadowLightData.EndSplitDistances.y); // Cascade projections are world-space; position comes in camera-relative. - float3 positionWS = position + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 positionWS = position + FrameBuffer::CameraPosAdjust.xyz; // Compute cascade blend factor with smoothstep float cascadeSelect = saturate((shadowMapDepth - directionalShadowLightData.StartSplitDistances.y) / (directionalShadowLightData.EndSplitDistances.x - directionalShadowLightData.StartSplitDistances.y)); diff --git a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli index 7a9d793453..caa6b41bf9 100644 --- a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli +++ b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli @@ -23,7 +23,7 @@ namespace WaterEffects return lerp(center.xxx, dispersed, 0.5); } - float3 ComputeCaustics(float4 waterData, float3 worldPosition, uint eyeIndex) + float3 ComputeCaustics(float4 waterData, float3 worldPosition) { float causticsDistToWater = waterData.w - worldPosition.z; float shoreFactorCaustics = saturate(causticsDistToWater / 64.0); @@ -32,7 +32,7 @@ namespace WaterEffects float causticsFade = 1.0 - saturate(causticsDistToWater / 1024.0); causticsFade *= causticsFade; - float2 causticsUV = (worldPosition.xy + FrameBuffer::CameraPosAdjust[eyeIndex].xy) * 0.005; + float2 causticsUV = (worldPosition.xy + FrameBuffer::CameraPosAdjust.xy) * 0.005; float2 dispersionOffset = float2(0.6, 0.8) * (0.025 * shoreFactorCaustics * saturate(causticsDistToWater / 256.0)); float2 causticsUV1 = PanCausticsUV(causticsUV, 0.5 * 0.2, 1.0); diff --git a/package/SKSE/Plugins/CommunityShaders/Overrides/README.md b/package/SKSE/Plugins/CommunityShaders/Overrides/README.md index a39e0cc4aa..907a6317af 100644 --- a/package/SKSE/Plugins/CommunityShaders/Overrides/README.md +++ b/package/SKSE/Plugins/CommunityShaders/Overrides/README.md @@ -111,7 +111,6 @@ To create feature-specific overrides, you need to use the correct feature short - `TerrainHelper` - Terrain Helper - `TerrainShadows` - Terrain Shadows - `VolumetricLighting` - Volumetric Lighting -- `VR` - VR - `WaterEffects` - Water Effects - `PerformanceOverlay` - Performance Overlay - `WetnessEffects` - Wetness Effects diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 4d6b3de2dd..d6c294423c 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -546,7 +546,6 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = Tailwind (wind behind player)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = Crosswind (left/right)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = Headwind (wind coming toward player)", - "feature.dynamic_cubemaps.advanced_vr_settings": "Advanced VR Settings", "feature.dynamic_cubemaps.color": "Color", "feature.dynamic_cubemaps.creator_info": "You must enable creator mode by adding the shader define CREATOR", "feature.dynamic_cubemaps.description": "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces.", @@ -558,12 +557,10 @@ "feature.dynamic_cubemaps.key_feature_1": "Real-time environment capture for realistic reflections", "feature.dynamic_cubemaps.key_feature_2": "Dynamic cube map generation based on camera position", "feature.dynamic_cubemaps.key_feature_3": "Enhanced water reflections with environmental details", - "feature.dynamic_cubemaps.key_feature_4": "Support for both standard and VR rendering modes", - "feature.dynamic_cubemaps.key_feature_5": "Optimized cubemap inference and irradiance calculation", + "feature.dynamic_cubemaps.key_feature_4": "Optimized cubemap inference and irradiance calculation", "feature.dynamic_cubemaps.name": "Dynamic Cubemaps", "feature.dynamic_cubemaps.roughness": "Roughness", "feature.dynamic_cubemaps.screen_space_reflections": "Screen Space Reflections", - "feature.dynamic_cubemaps.vr_restart_required": "A restart is required to enable in VR. Save Settings after enabling and restart the game.", "feature.exp_height_fog.apply_vanilla_fade": "Apply Vanilla Fade", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "Applies vanilla fade brightness to exponential height fog.", "feature.exp_height_fog.cubemap_mip_level": "Cubemap Mip Level", @@ -859,7 +856,6 @@ "feature.light_limit_fix.key_feature_2": "Unlimited dynamic lights", "feature.light_limit_fix.key_feature_3": "Improved lighting quality", "feature.light_limit_fix.key_feature_4": "Enhanced visual realism", - "feature.light_limit_fix.key_feature_5": "Enhanced visual realism", "feature.light_limit_fix.light_limit_vis": "Light Limit Visualization", "feature.light_limit_fix.lights_vis_mode": "Lights Visualisation Mode", "feature.light_limit_fix.lights_vis_mode_tooltip": " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n - Visualise the number of strict lights.\n - Visualise the number of clustered lights.\n - Visualize the Shadow Mask.\n", @@ -1058,11 +1054,9 @@ "feature.screen_space_gi.toggles": "Toggles", "feature.screen_space_gi.vanilla_ssao": "Vanilla SSAO", "feature.screen_space_gi.vanilla_ssao_tooltip": "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.", - "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "Vanilla SSAO is not supported in VR.", "feature.screen_space_gi.view_resize": "View Resize", "feature.screen_space_gi.visual": "Visual", "feature.screen_space_gi.visual_il": "Visual - IL", - "feature.screen_space_gi.vr_warning": "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects.", "feature.screen_space_shadows.bilinear_threshold": "Bilinear Threshold", "feature.screen_space_shadows.bilinear_threshold_tooltip": "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.", "feature.screen_space_shadows.description": "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\nThis technique adds fine-detail shadows that traditional shadow mapping might miss.", @@ -1081,8 +1075,6 @@ "feature.screen_space_shadows.shadow_contrast_tooltip": "Contrast boost for the shadow transition. Higher values produce harder shadow edges.", "feature.screen_space_shadows.surface_thickness": "Surface Thickness", "feature.screen_space_shadows.surface_thickness_tooltip": "Assumed thickness of surfaces for shadow detection. Lower values produce thinner, more precise shadows.", - "feature.screen_space_shadows.vr_stereo_sync": "VR Stereo Sync", - "feature.screen_space_shadows.vr_stereo_sync_tooltip": "Synchronizes shadow data between left and right eyes via bilateral reprojection and applies a depth-weighted blur to reduce per-eye noise. Uses min-blend so if either eye detects an occluder, the shadow is preserved. ", "feature.screenshot.apply_crop": "Apply crop", "feature.screenshot.async_note": "Capture and save run asynchronously without stalling the game.", "feature.screenshot.crop": "Crop", @@ -1096,7 +1088,7 @@ "feature.screenshot.name": "Screenshot", "feature.screenshot.open": "Open", "feature.screenshot.output": "Output", - "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR and VR captures use the lossless format selected below.", + "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR captures use the lossless format selected below.", "feature.screenshot.take_screenshot": "Take Screenshot Now", "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "A multiplier for the vanilla specular map, applied to the first layer's roughness", "feature.skin.adds_a_constant_layer_of_wetness_to_all": "Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort.", @@ -1361,7 +1353,6 @@ "feature.upscaling.method_none": "None", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "Upscaling", - "feature.upscaling.native_inputs": "Native Inputs", "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", "feature.upscaling.preset_balanced": "Balanced", "feature.upscaling.preset_dlaa": "DLAA", @@ -1376,15 +1367,12 @@ "feature.upscaling.streamline_logging_restart_note": "Changing this requires a restart to take effect.", "feature.upscaling.streamline_logging_tooltip": "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.", "feature.upscaling.upscale_preset": "Upscale Preset", - "feature.upscaling.upscaling_intermediates": "Upscaling Intermediates", "feature.upscaling.use_fps_limit": "Use FPS Limit", "feature.upscaling.use_fps_limit_tooltip_1": "Uses Reflex's internal FPS cap for steadier frametimes.", "feature.upscaling.use_fps_limit_tooltip_2": "Can lower latency versus uncapped rendering.", "feature.upscaling.use_markers_to_optimize": "Use Markers To Optimize", "feature.upscaling.use_markers_to_optimize_tooltip_1": "Uses frame markers for tighter Reflex timing.", "feature.upscaling.use_markers_to_optimize_tooltip_2": "Try On first; turn Off if it causes stutter on your setup.", - "feature.upscaling.view_resize": "View Resize", - "feature.upscaling.vr_intermediates_not_created": "VR intermediates not yet created (enter game world)", "feature.volumetric_lighting.description": "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\nThis adds dramatic god rays and atmospheric depth to both interior and exterior environments.", "feature.volumetric_lighting.enable_exteriors": "Enable Volumetric Lighting in Exteriors", "feature.volumetric_lighting.enable_interiors": "Enable Volumetric Lighting in Interiors", @@ -1412,31 +1400,6 @@ "feature.volumetric_shadows.key_feature_3": "Multi-cascade support", "feature.volumetric_shadows.key_feature_4": "Optimized for effects rendering", "feature.volumetric_shadows.name": "Volumetric Shadows", - "feature.vr.description": "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", - "feature.vr.key_feature_1": "Depth buffer culling optimization for VR performance", - "feature.vr.key_feature_2": "In-scene overlay menu with HMD/Controller/Fixed World attach modes", - "feature.vr.key_feature_3": "VR controller input with customizable button mappings", - "feature.vr.key_feature_4": "Grip-to-drag overlay positioning with depth control", - "feature.vr.key_feature_5": "Configurable occlusion culling parameters", - "feature.vr.key_feature_6": "Enhanced VR compatibility with SteamVR and OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "Debug", - "feature.vr_stereo.debug_pom_depth": "Debug POM Depth", - "feature.vr_stereo.disocclusion_depth_threshold": "Disocclusion Depth Threshold", - "feature.vr_stereo.enable": "Enable", - "feature.vr_stereo.enable_stereo_reprojection": "Enable Stereo Reprojection", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.", - "feature.vr_stereo.forward_occlusion_scale": "Forward Occlusion Scale", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.", - "feature.vr_stereo.full_blend_depth_view": "Full Blend Depth View", - "feature.vr_stereo.full_blend_distance": "Full Blend Distance", - "feature.vr_stereo.full_blend_distance_tooltip": "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.", - "feature.vr_stereo.full_blend_zone_hint": " Cyan = full blend zone (closer = stronger tint)", - "feature.vr_stereo.off": "Off", - "feature.vr_stereo.pom_depth_scale": "POM Depth Scale", - "feature.vr_stereo.pom_depth_scale_tooltip": "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.", - "feature.vr_stereo.restart_required": "Restart is required to enable VR stereo reprojection.", - "feature.vr_stereo.skip_pixel_reprojection": "Skip Pixel Reprojection", "feature.water_effects.description": "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\nThis feature adds dynamic light patterns and improved water visual quality.", "feature.water_effects.key_feature_1": "Realistic water caustics", "feature.water_effects.key_feature_2": "Enhanced underwater lighting", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 30264ab701..a5ce43434a 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -545,7 +545,6 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = 顺风(风在玩家背后)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = 横风(左/右)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = 逆风(风朝向玩家)", - "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", "feature.dynamic_cubemaps.color": "颜色", "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", @@ -557,12 +556,10 @@ "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", "feature.dynamic_cubemaps.key_feature_3": "增强的水面反射,包含环境细节", - "feature.dynamic_cubemaps.key_feature_4": "支持标准和VR两种渲染模式", - "feature.dynamic_cubemaps.key_feature_5": "优化的立方体贴图推理与辐照度计算", + "feature.dynamic_cubemaps.key_feature_4": "优化的立方体贴图推理与辐照度计算", "feature.dynamic_cubemaps.name": "动态立方体贴图", "feature.dynamic_cubemaps.roughness": "粗糙度", "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", - "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", @@ -858,7 +855,6 @@ "feature.light_limit_fix.key_feature_2": "无限动态光源", "feature.light_limit_fix.key_feature_3": "提升光照质量", "feature.light_limit_fix.key_feature_4": "增强视觉真实感", - "feature.light_limit_fix.key_feature_5": "增强视觉真实感", "feature.light_limit_fix.light_limit_vis": "光源限制可视化", "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", @@ -1057,11 +1053,9 @@ "feature.screen_space_gi.toggles": "开关", "feature.screen_space_gi.vanilla_ssao": "原版SSAO", "feature.screen_space_gi.vanilla_ssao_tooltip": "启用Skyrim内置的SSAO。使用SSGI时通常禁用此选项以避免双重变暗。", - "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "VR不支持原版SSAO。", "feature.screen_space_gi.view_resize": "视图调整大小", "feature.screen_space_gi.visual": "视觉", "feature.screen_space_gi.visual_il": "视觉 - IL", - "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", @@ -1080,8 +1074,6 @@ "feature.screen_space_shadows.shadow_contrast_tooltip": "阴影过渡的对比度增强。较高值产生更硬的阴影边缘。", "feature.screen_space_shadows.surface_thickness": "表面厚度", "feature.screen_space_shadows.surface_thickness_tooltip": "阴影检测的假设表面厚度。较低值产生更薄、更精确的阴影。", - "feature.screen_space_shadows.vr_stereo_sync": "VR立体同步", - "feature.screen_space_shadows.vr_stereo_sync_tooltip": "通过双向重投影同步左右眼之间的阴影数据,并应用深度加权模糊以减少每只眼睛的噪点。使用最小混合,如果任一眼睛检测到遮挡物,则保留阴影。", "feature.screenshot.apply_crop": "应用裁剪", "feature.screenshot.async_note": "捕获和保存异步运行,不会让游戏停顿。", "feature.screenshot.crop": "裁剪", @@ -1360,7 +1352,6 @@ "feature.upscaling.method_none": "无", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "超分辨率", - "feature.upscaling.native_inputs": "原生输入", "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", "feature.upscaling.preset_balanced": "平衡", "feature.upscaling.preset_dlaa": "DLAA", @@ -1375,15 +1366,12 @@ "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", "feature.upscaling.upscale_preset": "升频预设", - "feature.upscaling.upscaling_intermediates": "升频中间结果", "feature.upscaling.use_fps_limit": "使用FPS限制", "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", "feature.upscaling.use_fps_limit_tooltip_2": "相比无上限渲染可以降低延迟。", "feature.upscaling.use_markers_to_optimize": "使用标记优化", "feature.upscaling.use_markers_to_optimize_tooltip_1": "使用帧标记以实现更紧密的Reflex计时。", "feature.upscaling.use_markers_to_optimize_tooltip_2": "先尝试开启;如果在您的设置上造成卡顿则关闭。", - "feature.upscaling.view_resize": "视图调整大小", - "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", @@ -1411,31 +1399,6 @@ "feature.volumetric_shadows.key_feature_3": "多级联支持", "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", "feature.volumetric_shadows.name": "体积阴影", - "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", - "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", - "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", - "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", - "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", - "feature.vr.key_feature_5": "可配置的遮挡剔除参数", - "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "调试", - "feature.vr_stereo.debug_pom_depth": "调试POM深度", - "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", - "feature.vr_stereo.enable": "启用", - "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", - "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", - "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", - "feature.vr_stereo.full_blend_distance": "完全混合距离", - "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", - "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", - "feature.vr_stereo.off": "关闭", - "feature.vr_stereo.pom_depth_scale": "POM深度缩放", - "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", - "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", - "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", "feature.water_effects.key_feature_1": "逼真的水面焦散", "feature.water_effects.key_feature_2": "增强的水下光照", diff --git a/package/Shaders/Common/FrameBuffer.hlsli b/package/Shaders/Common/FrameBuffer.hlsli index 68f0f90371..a82adafd78 100644 --- a/package/Shaders/Common/FrameBuffer.hlsli +++ b/package/Shaders/Common/FrameBuffer.hlsli @@ -6,19 +6,18 @@ namespace FrameBuffer cbuffer PerFrame : register(b12) { -#if !defined(VR) - row_major float4x4 CameraView[1] : packoffset(c0); - row_major float4x4 CameraProj[1] : packoffset(c4); - row_major float4x4 CameraViewProj[1] : packoffset(c8); - row_major float4x4 CameraViewProjUnjittered[1] : packoffset(c12); - row_major float4x4 CameraPreviousViewProjUnjittered[1] : packoffset(c16); - row_major float4x4 CameraProjUnjittered[1] : packoffset(c20); - row_major float4x4 CameraProjUnjitteredInverse[1] : packoffset(c24); - row_major float4x4 CameraViewInverse[1] : packoffset(c28); - row_major float4x4 CameraViewProjInverse[1] : packoffset(c32); - row_major float4x4 CameraProjInverse[1] : packoffset(c36); - float4 CameraPosAdjust[1] : packoffset(c40); - float4 CameraPreviousPosAdjust[1] : packoffset(c41); // fDRClampOffset in w + row_major float4x4 CameraView : packoffset(c0); + row_major float4x4 CameraProj : packoffset(c4); + row_major float4x4 CameraViewProj : packoffset(c8); + row_major float4x4 CameraViewProjUnjittered : packoffset(c12); + row_major float4x4 CameraPreviousViewProjUnjittered : packoffset(c16); + row_major float4x4 CameraProjUnjittered : packoffset(c20); + row_major float4x4 CameraProjUnjitteredInverse : packoffset(c24); + row_major float4x4 CameraViewInverse : packoffset(c28); + row_major float4x4 CameraViewProjInverse : packoffset(c32); + row_major float4x4 CameraProjInverse : packoffset(c36); + float4 CameraPosAdjust : packoffset(c40); + float4 CameraPreviousPosAdjust : packoffset(c41); // fDRClampOffset in w float4 FrameParams : packoffset(c42); // inverse fGamma in x, some flags in yzw float4 DynamicResolutionParams1 : packoffset(c43); // fDynamicResolutionWidthRatio in x, // fDynamicResolutionHeightRatio in y, @@ -28,29 +27,6 @@ namespace FrameBuffer // fDynamicResolutionHeightRatio in y, // fDynamicResolutionWidthRatio - fDRClampOffset in z, // fDynamicResolutionPreviousWidthRatio - fDRClampOffset in w -#else - row_major float4x4 CameraView[2] : packoffset(c0); - row_major float4x4 CameraProj[2] : packoffset(c8); - row_major float4x4 CameraViewProj[2] : packoffset(c16); - row_major float4x4 CameraViewProjUnjittered[2] : packoffset(c24); - row_major float4x4 CameraPreviousViewProjUnjittered[2] : packoffset(c32); - row_major float4x4 CameraProjUnjittered[2] : packoffset(c40); - row_major float4x4 CameraProjUnjitteredInverse[2] : packoffset(c48); - row_major float4x4 CameraViewInverse[2] : packoffset(c56); - row_major float4x4 CameraViewProjInverse[2] : packoffset(c64); - row_major float4x4 CameraProjInverse[2] : packoffset(c72); - float4 CameraPosAdjust[2] : packoffset(c80); - float4 CameraPreviousPosAdjust[2] : packoffset(c82); // fDRClampOffset in w - float4 FrameParams : packoffset(c84); // inverse fGamma in x, some flags in yzw - float4 DynamicResolutionParams1 : packoffset(c85); // fDynamicResolutionWidthRatio in x, - // fDynamicResolutionHeightRatio in y, - // fDynamicResolutionPreviousWidthRatio in z, - // fDynamicResolutionPreviousHeightRatio in w - float4 DynamicResolutionParams2 : packoffset(c86); // inverse fDynamicResolutionWidthRatio in x, inverse - // fDynamicResolutionHeightRatio in y, - // fDynamicResolutionWidthRatio - fDRClampOffset in z, - // fDynamicResolutionPreviousWidthRatio - fDRClampOffset in w -#endif // !VR } /** @@ -60,37 +36,23 @@ namespace FrameBuffer * space by custom math (for example, after jitter removal or other UV manipulation). * This function only clamps; it does not apply dynamic-resolution scaling. * - * In VR, clamping is restricted to the current eye half to avoid cross-eye sampling. - * * @param[in] screenPositionDR UVs already expressed in dynamic-resolution space. - * @param[in] screenPosition Original normalized screen UVs (used to infer eye in VR). - * @param[in] stereo Whether to apply stereo eye-half clamping in VR. Default is 1. + * @param[in] screenPosition Original normalized screen UVs. * @return Clamped dynamic-resolution UVs. */ - float2 ClampDynamicResolutionAdjustedScreenPosition(float2 screenPositionDR, float2 screenPosition, uint stereo = 1) + float2 ClampDynamicResolutionAdjustedScreenPosition(float2 screenPositionDR, float2 screenPosition) { float2 minValue = 0; float2 maxValue = float2(DynamicResolutionParams2.z, DynamicResolutionParams1.y); -#if defined(VR) - // VR uses side-by-side stereo packing in the shared render target. - // Clamp within the current eye's half to avoid cross-eye sampling. - if (stereo) { - bool isRight = screenPosition.x >= 0.5; - float minFactor = isRight ? 1 : 0; - minValue.x = 0.5 * (DynamicResolutionParams2.z * minFactor); - float maxFactor = isRight ? 2 : 1; - maxValue.x = 0.5 * (DynamicResolutionParams2.z * maxFactor); - } -#endif return clamp(screenPositionDR, minValue, maxValue); } - // Projects a world-space (camera-relative) point into NDC using the eye's CameraViewProj + // Projects a world-space (camera-relative) point into NDC using CameraViewProj // and returns the post-perspective z (NDC depth). Combine with SharedData::GetScreenDepth // to get a linear view-space distance suitable for cascade-split comparisons. - float GetShadowDepth(float3 positionWS, uint eyeIndex) + float GetShadowDepth(float3 positionWS) { - float4 positionCS = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1)); + float4 positionCS = mul(FrameBuffer::CameraViewProj, float4(positionWS, 1)); return positionCS.z / positionCS.w; } @@ -104,13 +66,12 @@ namespace FrameBuffer * `ClampDynamicResolutionAdjustedScreenPosition(...)` instead. * * @param[in] screenPosition Normalized screen UVs in non-DR space. - * @param[in] stereo Whether to apply stereo eye-half clamping in VR. Default is 1. * @return Dynamic-resolution-adjusted and clamped UVs. */ - float2 GetDynamicResolutionAdjustedScreenPosition(float2 screenPosition, uint stereo = 1) + float2 GetDynamicResolutionAdjustedScreenPosition(float2 screenPosition) { float2 screenPositionDR = DynamicResolutionParams1.xy * screenPosition; - return ClampDynamicResolutionAdjustedScreenPosition(screenPositionDR, screenPosition, stereo); + return ClampDynamicResolutionAdjustedScreenPosition(screenPositionDR, screenPosition); } /** @@ -118,9 +79,9 @@ namespace FrameBuffer * * Applies dynamic-resolution adjustment/clamp to XY and preserves Z unchanged. */ - float3 GetDynamicResolutionAdjustedScreenPosition(float3 screenPositionDR, uint stereo = 1) + float3 GetDynamicResolutionAdjustedScreenPosition(float3 screenPositionDR) { - return float3(GetDynamicResolutionAdjustedScreenPosition(screenPositionDR.xy, stereo), screenPositionDR.z); + return float3(GetDynamicResolutionAdjustedScreenPosition(screenPositionDR.xy), screenPositionDR.z); } /** @@ -146,13 +107,6 @@ namespace FrameBuffer float2 screenPositionDR = DynamicResolutionParams1.zw * screenPosition; float2 minValue = 0; float2 maxValue = float2(DynamicResolutionParams2.w, DynamicResolutionParams1.w); -#if defined(VR) - bool isRight = screenPosition.x >= 0.5; - float minFactor = isRight ? 1 : 0; - minValue.x = 0.5 * (DynamicResolutionParams2.w * minFactor); - float maxFactor = isRight ? 2 : 1; - maxValue.x = 0.5 * (DynamicResolutionParams2.w * maxFactor); -#endif return clamp(screenPositionDR, minValue, maxValue); } @@ -161,22 +115,22 @@ namespace FrameBuffer return pow(abs(linearColor), FrameParams.x); } - float3 WorldToView(float3 x, bool is_position = true, uint a_eyeIndex = 0) + float3 WorldToView(float3 x, bool is_position = true) { float4 newPosition = float4(x, (float)is_position); - return mul(CameraView[a_eyeIndex], newPosition).xyz; + return mul(CameraView, newPosition).xyz; } - float3 ViewToWorld(float3 x, bool is_position = true, uint a_eyeIndex = 0) + float3 ViewToWorld(float3 x, bool is_position = true) { float4 newPosition = float4(x, (float)is_position); - return mul(CameraViewInverse[a_eyeIndex], newPosition).xyz; + return mul(CameraViewInverse, newPosition).xyz; } - float2 ViewToUV(float3 x, bool is_position = true, uint a_eyeIndex = 0) + float2 ViewToUV(float3 x, bool is_position = true) { float4 newPosition = float4(x, (float)is_position); - float4 uv = mul(CameraProj[a_eyeIndex], newPosition); + float4 uv = mul(CameraProj, newPosition); return (uv.xy / uv.w) * float2(0.5f, -0.5f) + 0.5f; } diff --git a/package/Shaders/Common/MotionBlur.hlsli b/package/Shaders/Common/MotionBlur.hlsli index 36ba16fb45..a6cca62ace 100644 --- a/package/Shaders/Common/MotionBlur.hlsli +++ b/package/Shaders/Common/MotionBlur.hlsli @@ -5,10 +5,10 @@ namespace MotionBlur { - float2 GetSSMotionVector(float4 a_wsPosition, float4 a_previousWSPosition, uint a_eyeIndex = 0) + float2 GetSSMotionVector(float4 a_wsPosition, float4 a_previousWSPosition) { - float4 screenPosition = mul(FrameBuffer::CameraViewProjUnjittered[a_eyeIndex], a_wsPosition); - float4 previousScreenPosition = mul(FrameBuffer::CameraPreviousViewProjUnjittered[a_eyeIndex], a_previousWSPosition); + float4 screenPosition = mul(FrameBuffer::CameraViewProjUnjittered, a_wsPosition); + float4 previousScreenPosition = mul(FrameBuffer::CameraPreviousViewProjUnjittered, a_previousWSPosition); screenPosition.xy = screenPosition.xy / screenPosition.ww; previousScreenPosition.xy = previousScreenPosition.xy / previousScreenPosition.ww; return float2(-0.5, 0.5) * (screenPosition.xy - previousScreenPosition.xy); diff --git a/package/Shaders/Common/ShadowSampling.hlsli b/package/Shaders/Common/ShadowSampling.hlsli index 70e7143f54..8397d5333b 100644 --- a/package/Shaders/Common/ShadowSampling.hlsli +++ b/package/Shaders/Common/ShadowSampling.hlsli @@ -49,7 +49,7 @@ namespace ShadowSampling return SharedData::HasDirectionalShadows; } - float GetWorldShadow(float3 positionWS, float3 offset, uint eyeIndex) + float GetWorldShadow(float3 positionWS, float3 offset) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0; @@ -66,7 +66,7 @@ namespace ShadowSampling return worldShadow; } - float Get3DFilteredShadow(float3 positionWS, float3 viewDirection, float2 screenPosition, uint eyeIndex, out float surfaceShadow) + float Get3DFilteredShadow(float3 positionWS, float3 viewDirection, float2 screenPosition, out float surfaceShadow) { #if defined(EFFECT) float viewRayLength = min(Permutation::EffectRadius * 0.2, 256); @@ -95,7 +95,7 @@ namespace ShadowSampling for (uint i = 0; i < sampleCount; i++) { float t = (float(i) + noise) * rcpSampleCount; float3 sampledPositionWS = lerp(endPosition, startPosition, t); - float worldShadowSample = ShadowSampling::GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + float worldShadowSample = ShadowSampling::GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust.xyz); surfaceShadow = worldShadowSample; worldShadow += worldShadowSample; } @@ -108,7 +108,7 @@ namespace ShadowSampling #if defined(VOLUMETRIC_SHADOWS) if (HasDirectionalShadows()) { float vsmSurfaceShadow; - float shadow = VolumetricShadows::GetVSMShadow3D(startPosition, endPosition, noise, sampleCount, eyeIndex, vsmSurfaceShadow); + float shadow = VolumetricShadows::GetVSMShadow3D(startPosition, endPosition, noise, sampleCount, vsmSurfaceShadow); surfaceShadow *= vsmSurfaceShadow; return worldShadow * shadow; } @@ -119,7 +119,7 @@ namespace ShadowSampling return worldShadow; } - float GetLightingShadow(float3 worldPosition, uint eyeIndex, out float detailedShadow) + float GetLightingShadow(float3 worldPosition, out float detailedShadow) { if (!HasDirectionalShadows()) { detailedShadow = 1.0; @@ -127,7 +127,7 @@ namespace ShadowSampling } #if defined(VOLUMETRIC_SHADOWS) - float shadow = VolumetricShadows::GetVSMShadow2D(worldPosition, eyeIndex, detailedShadow); + float shadow = VolumetricShadows::GetVSMShadow2D(worldPosition, detailedShadow); return shadow; #else detailedShadow = 1.0; diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index c848328844..f7221a063f 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -3,7 +3,6 @@ #include "Common/FrameBuffer.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" -#include "Common/VR.hlsli" namespace SharedData { @@ -31,7 +30,7 @@ namespace SharedData bool InMapMenu; // If the world/local map is open (note that the renderer is still deferred here) bool HideSky; // HideSky flag in WorldSpace, e.g. Blackreach float MipBias; // Offset to mip level for TAA sharpness - float WaterSystemHeight; // TES::GetWaterHeight at eye-0 in camera-relative Z; -FLT_MAX when no water body found (VR only) + float WaterSystemHeight; // TES::GetWaterHeight in camera-relative Z; -FLT_MAX when no water body found float3 pad0; float4 AmbientSHR; float4 AmbientSHG; @@ -336,17 +335,16 @@ namespace SharedData Texture2D DepthTexture : register(t17); // Get a int3 to be used as texture sample coord. [0,1] in uv space - int3 ConvertUVToSampleCoord(float2 uv, uint a_eyeIndex) + int3 ConvertUVToSampleCoord(float2 uv) { - uv = Stereo::ConvertToStereoUV(uv, a_eyeIndex); uv = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); return int3(uv * BufferDim.xy, 0); } // Get a raw depth from the depth buffer. [0,1] in uv space - float GetDepth(float2 uv, uint a_eyeIndex = 0) + float GetDepth(float2 uv) { - return DepthTexture.Load(ConvertUVToSampleCoord(uv, a_eyeIndex)).x; + return DepthTexture.Load(ConvertUVToSampleCoord(uv)).x; } float GetScreenDepth(float depth) @@ -359,19 +357,16 @@ namespace SharedData return (CameraData.w / (-depths * CameraData.z + CameraData.x)); } - float GetScreenDepth(float2 uv, uint a_eyeIndex = 0) + float GetScreenDepth(float2 uv) { - float depth = GetDepth(uv, a_eyeIndex); + float depth = GetDepth(uv); return GetScreenDepth(depth); } // Returns water data for the tile containing worldPosition (camera-relative XY). - // The .w component (water surface height) is stored in C++ as camera-relative Z of - // eye 0 (left eye). Pass eyeIndex to have .w corrected into the current eye's - // camera-relative frame; defaults to 0 (no correction, backwards-compatible). - float4 GetWaterData(float3 worldPosition, uint eyeIndex = 0) + float4 GetWaterData(float3 worldPosition) { - float2 cellF = (((worldPosition.xy + FrameBuffer::CameraPosAdjust[0].xy)) / 4096.0) + 64.0; // always positive + float2 cellF = (((worldPosition.xy + FrameBuffer::CameraPosAdjust.xy)) / 4096.0) + 64.0; // always positive int2 cellInt; float2 cellFrac = modf(cellF, cellInt); @@ -387,12 +382,6 @@ namespace SharedData [flatten] if (cellInt.x < 5 && cellInt.x >= 0 && cellInt.y < 5 && cellInt.y >= 0) waterData = WaterData[waterTile]; -# if defined(VR) - // Correct .w from eye-0 camera-relative Z to the current eye's camera-relative Z. - // No-op when eyeIndex == 0 (both terms are identical). - waterData.w += FrameBuffer::CameraPosAdjust[0].z - FrameBuffer::CameraPosAdjust[eyeIndex].z; -# endif - return waterData; } diff --git a/package/Shaders/Common/VR.hlsli b/package/Shaders/Common/VR.hlsli deleted file mode 100644 index 0b8ea117ba..0000000000 --- a/package/Shaders/Common/VR.hlsli +++ /dev/null @@ -1,668 +0,0 @@ -#ifndef __VR_DEPENDENCY_HLSL__ -#define __VR_DEPENDENCY_HLSL__ -#ifdef VR - -// First person model depth threshold for VR occlusion logic -# ifndef VR_FP_Z -# define VR_FP_Z 18.0 -# endif - -# if defined(VSHADER) -# include "Common/Math.hlsli" -# endif // VSHADER - -# if (!defined(COMPUTESHADER) && !defined(CSHADER)) || defined(FRAMEBUFFER) -# include "Common/FrameBuffer.hlsli" -# endif -cbuffer VRValues : register(b13) -{ - float AlphaTestRefRS : packoffset(c0); - float StereoEnabled : packoffset(c0.y); - float2 EyeOffsetScale : packoffset(c0.z); - float4 EyeClipEdge[2] : packoffset(c1); -} -#endif - -namespace Stereo -{ -#ifdef VR_STEREO_OPT - /// Sentinel written to PomOffsetTex when a pixel's Lighting PS did not run POM. - /// Convention: -1.0 = no POM; >= 0.0 = POM ran (StereoBlendCS detects by sign). - /// Must match kPomOffsetNoData in VRStereoOptimizations.h. - static const float POM_NO_DATA = -1.0; -#endif - /** - Converts to the eye specific uv [0,1]. - In VR, texture buffers include the left and right eye in the same buffer. Flat - only has a single camera for the entire width. This means the x value [0, .5] - represents the left eye, and the x value (.5, 1] are the right eye. This returns - the adjusted value - @param uv - uv coords [0,1] to be encoded for VR - @param a_eyeIndex The eyeIndex; 0 is left, 1 is right - @param a_invertY Whether to invert the Y direction - @returns uv with x coords adjusted for the VR texture buffer - */ - float2 ConvertToStereoUV(float2 uv, uint a_eyeIndex, uint a_invertY = 0) - { -#ifdef VR - // convert [0,1] to eye specific [0,.5] and [.5, 1] dependent on a_eyeIndex - uv.x = saturate(uv.x); - uv.x = (uv.x + (float)a_eyeIndex) / 2; - if (a_invertY) - uv.y = 1 - uv.y; -#endif - return uv; - } - - float3 ConvertToStereoUV(float3 uv, uint a_eyeIndex, uint a_invertY = 0) - { - uv.xy = ConvertToStereoUV(uv.xy, a_eyeIndex, a_invertY); - return uv; - } - - float4 ConvertToStereoUV(float4 uv, uint a_eyeIndex, uint a_invertY = 0) - { - uv.xy = ConvertToStereoUV(uv.xy, a_eyeIndex, a_invertY); - return uv; - } - - /** - Converts from eye specific uv to general uv [0,1]. - In VR, texture buffers include the left and right eye in the same buffer. - This means the x value [0, .5] represents the left eye, and the x value (.5, 1] are the right eye. - This returns the adjusted value - @param uv - eye specific uv coords [0,1]; if uv.x < 0.5, it's a left eye; otherwise right - @param a_eyeIndex The eyeIndex; 0 is left, 1 is right - @param a_invertY Whether to invert the Y direction - @returns uv with x coords adjusted to full range for either left or right eye - */ - float2 ConvertFromStereoUV(float2 uv, uint a_eyeIndex, uint a_invertY = 0) - { -#ifdef VR - // convert [0,.5] to [0, 1] and [.5, 1] to [0,1] - uv.x = 2 * uv.x - (float)a_eyeIndex; - if (a_invertY) - uv.y = 1 - uv.y; -#endif - return uv; - } - - float3 ConvertFromStereoUV(float3 uv, uint a_eyeIndex, uint a_invertY = 0) - { - uv.xy = ConvertFromStereoUV(uv.xy, a_eyeIndex, a_invertY); - return uv; - } - - float4 ConvertFromStereoUV(float4 uv, uint a_eyeIndex, uint a_invertY = 0) - { - uv.xy = ConvertFromStereoUV(uv.xy, a_eyeIndex, a_invertY); - return uv; - } - - /** - Gets the eyeIndex for Compute Shaders - @param texCoord Texcoord on the screen [0,1] - @returns eyeIndex (0 left, 1 right) - */ - uint GetEyeIndexFromTexCoord(float2 texCoord) - { -#ifdef VR - return (texCoord.x >= 0.5) ? 1 : 0; -#endif // VR - return 0; - } - - /** - * @brief Applies motion velocity to UV coordinates and determines if the resulting mono UV is out of screen bounds. - * @param uv Screen UV coordinates (stereo in VR, mono in SE) - * @param velocity Delta motion mapping - * @param isOutOfBounds Output flag indicating if the motion went out of bounds - * @return Newly displaced UV coordinate mapped back to correct space (stereo in VR, mono in SE). Clamped if necessary. - */ - float2 ApplyVelocityToUV(float2 uv, float2 velocity, out bool isOutOfBounds) - { - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - float2 prevUVmono = Stereo::ConvertFromStereoUV(uv, eyeIndex) + velocity; - float2 clampedMono = prevUVmono; - -#ifdef VR - // VR logic: mono.x < 0 is clamped to 0, not rejected. OOB fires for mono.x >= 1 or mono.y outside [0, 1] inclusive. - isOutOfBounds = (prevUVmono.x >= 1.0) || (prevUVmono.y <= 0.0) || (prevUVmono.y >= 1.0); - clampedMono.x = saturate(prevUVmono.x); -#else - // SE logic: inclusive boundaries on both sides. - isOutOfBounds = any(prevUVmono >= 1.0) || any(prevUVmono <= 0.0); -#endif - - return Stereo::ConvertToStereoUV(clampedMono, eyeIndex); - } - - /** - Converts to the eye specific screenposition [0,Resolution]. - In VR, texture buffers include the left and right eye in the same buffer. Flat only has a single camera for the entire width. - This means the x value [0, resx/2] represents the left eye, and the x value (resx/2, x] are the right eye. - This returns the adjusted value - @param screenPosition - Screenposition coords ([0,resx], [0,resy]) to be encoded for VR - @param a_eyeIndex The eyeIndex; 0 is left, 1 is right - @param a_resolution The resolution of the screen - @returns screenPosition with x coords adjusted for the VR texture buffer - */ - float2 ConvertToStereoSP(float2 screenPosition, uint a_eyeIndex, float2 a_resolution) - { - screenPosition.x /= a_resolution.x; - float2 stereoUV = ConvertToStereoUV(screenPosition, a_eyeIndex); - return stereoUV * a_resolution; - } - - float3 ConvertToStereoSP(float3 screenPosition, uint a_eyeIndex, float2 a_resolution) - { - float2 xy = screenPosition.xy / a_resolution; - xy = ConvertToStereoUV(xy, a_eyeIndex); - return float3(xy * a_resolution, screenPosition.z); - } - - float4 ConvertToStereoSP(float4 screenPosition, uint a_eyeIndex, float2 a_resolution) - { - float2 xy = screenPosition.xy / a_resolution; - xy = ConvertToStereoUV(xy, a_eyeIndex); - return float4(xy * a_resolution, screenPosition.zw); - } - - /** - * @brief Converts UV coordinates from the range [0, 1] to normalized screen space [-1, 1]. - * - * This function takes texture coordinates and transforms them into a normalized - * coordinate system centered at the origin. This is useful for various graphical - * calculations, especially in shaders that require symmetry around the center. - * - * @param uv The input UV coordinates in the range [0, 1]. - * @return float2 Normalized screen space coordinates in the range [-1, 1]. - */ - float2 ConvertUVToNormalizedScreenSpace(float2 uv) - { - float2 normalizedCoord; - normalizedCoord.x = 2.0 * (-0.5 + abs(2.0 * (uv.x - 0.5))); // Convert UV.x - normalizedCoord.y = 2.0 * uv.y - 1.0; // Convert UV.y - return normalizedCoord; - } - - /** - * @brief Returns the maximum absolute depth difference between a center depth and four neighbors. - * - * Used for depth-discontinuity edge detection in stereo sync passes. - * Works with both NDC depths (fixed absolute threshold) and linear view-space depths - * (relative threshold: divide result by max(center, 1.0)). - * - * @param[in] center Depth at the pixel being tested. - * @param[in] neighbors Depths at four neighboring pixels (e.g. ±1 or ±2 cross pattern). - * @return Maximum of |center - neighbor| across all four samples. - */ - float MaxDepthDiff(float center, float4 neighbors) - { - return max(max(abs(center - neighbors.x), abs(center - neighbors.y)), - max(abs(center - neighbors.z), abs(center - neighbors.w))); - } - - /** - * @brief Clamps a stereo UV coordinate to the eye-local X range of the packed stereo buffer. - * - * Prevents cross-neighbor UV samples from crossing the x=0.5 seam into the other eye's - * region of the side-by-side stereo texture. Y is not clamped; sampler address modes - * handle vertical out-of-bounds. - * - * @param[in] uv Stereo UV coordinate to clamp. - * @param[in] eyeIndex Eye index (0 = left [0, 0.5], 1 = right [0.5, 1]). - * @return UV with x restricted to eyeIndex's half of the stereo buffer. - */ - float2 ClampToEyeUV(float2 uv, uint eyeIndex) - { - uv.x = clamp(uv.x, eyeIndex == 0 ? 0.0f : 0.5f, eyeIndex == 0 ? 0.5f : 1.0f); - return uv; - } - - /** - * @brief Clamps a pixel coordinate to the eye-local X bounds of the packed stereo buffer. - * - * Prevents cross-neighbor pixel reads from crossing the half-width seam into the - * other eye's region of the side-by-side stereo texture. - * - * @param[in] px Pixel coordinate to clamp. - * @param[in] eyeIndex Eye index (0 = left, 1 = right). - * @param[in] frameDim Full stereo buffer dimensions (width covers both eyes). - * @return Clamped pixel coordinate, restricted to eyeIndex's half of the buffer. - */ - int2 ClampToEyeBounds(int2 px, uint eyeIndex, float2 frameDim) - { - int halfWidth = (int)((uint)frameDim.x >> 1); - px.x = clamp(px.x, eyeIndex == 0 ? 0 : halfWidth, eyeIndex == 0 ? (halfWidth - 1) : ((int)frameDim.x - 1)); - px.y = clamp(px.y, 0, (int)frameDim.y - 1); - return px; - } - -#if defined(PSHADER) || defined(FRAMEBUFFER) - // These functions require the framebuffer which is typically provided with the PSHADER - /** - Gets the eyeIndex for PSHADER - @returns eyeIndex (0 left, 1 right) - */ - uint GetEyeIndexPS(float4 position, float4 offset = 0.0.xxxx) - { -# if !defined(VR) - uint eyeIndex = 0; -# else - float4 stereoUV; - stereoUV.xy = position.xy * offset.xy + offset.zw; - stereoUV.x = FrameBuffer::DynamicResolutionParams2.x * stereoUV.x; - stereoUV.x = (stereoUV.x >= 0.5); - uint eyeIndex = (uint)(((int)((uint)StereoEnabled)) * (int)stereoUV.x); -# endif - return eyeIndex; - } - - /** - * @brief Checks if the color is non zero by testing if the color is greater than 0 by epsilon. - * - * This function check is a color is non black. It uses a small epsilon value to allow for - * floating point imprecision. - * - * For screen-space reflection (SSR), this acts as a mask and checks for an invalid reflection by - * checking if the reflection color is essentially black (close to zero). - * - * @param[in] ssrColor The color to check. - * @param[in] epsilon Small tolerance value used to determine if the color is close to zero. - * @return True if color is non zero, otherwise false. - */ - bool IsNonZeroColor(float4 ssrColor, float epsilon = 0.001) - { - return dot(ssrColor.xyz, ssrColor.xyz) > epsilon * epsilon; - } - -# ifdef VR - /** - * @brief Converts mono UV coordinates from one eye to the corresponding mono UV coordinates of the other eye. - * - * This function is used to transition UV coordinates from one eye's perspective to the other eye in a stereo rendering setup. - * It operates by converting the mono UV to clip space, transforming it into world space, and then reprojecting it - * into the other eye's clip space before converting back to UV coordinates. It supports dynamic resolution adjustments - * and applies eye offset adjustments for correct stereo separation. - * - * The function considers the aspect of VR by modifying the NDC to view space conversion based on the stereo setup, - * ensuring accurate rendering across both eyes. - * - * @param[in] monoUV The UV coordinates and depth value (Z component) for the current eye, in the range [0,1]. - * @param[in] eyeIndex Index of the source/current eye (0 for left, 1 for right). - * @param[in] dynamicres Optional flag indicating whether dynamic resolution is applied. Default is false. - * @return UV coordinates adjusted to the other eye, with depth. - */ - float3 ConvertMonoUVToOtherEye(float3 monoUV, uint eyeIndex, bool dynamicres = false) - { - // Convert from dynamic res to true UV space if necessary - if (dynamicres) - monoUV.xy *= FrameBuffer::DynamicResolutionParams2.xy; - - // Convert UV to Clip Space - float4 clipPos = float4(monoUV.xy * float2(2, -2) - float2(1, -1), monoUV.z, 1); - - // Convert Clip Space to World Space for the current eye - float4 worldPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], clipPos); - worldPos /= worldPos.w; - - // Apply eye offset adjustment in world space - worldPos.xyz += FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[1 - eyeIndex].xyz; - - // Convert World Space to Clip Space for the other eye - float4 clipPosOtherEye = mul(FrameBuffer::CameraViewProj[1 - eyeIndex], worldPos); - clipPosOtherEye /= clipPosOtherEye.w; - - // Convert Clip Space to UV (Y is flipped: clip +1 = top, UV 0 = top) - float3 monoUVOtherEye = float3(clipPosOtherEye.xy * float2(0.5f, -0.5f) + 0.5f, clipPosOtherEye.z); - - // Convert back to dynamic res space if necessary - if (dynamicres) - monoUVOtherEye.xy *= FrameBuffer::DynamicResolutionParams1.xy; - - return monoUVOtherEye; - } -# endif // VR - - /** - * @brief Resolves a mono UV to the eye that can see it, crossing to the other eye if needed. - * - * When a screen-space ray or sample position leaves the current eye's screen bounds, - * this function tries to find the corresponding location in the other eye via - * ConvertMonoUVToOtherEye. On flat (non-VR) this is a no-op: sampleUV and - * sampleEyeIndex are set to the input values unchanged. - * - * Based on concepts from https://cuteloong.github.io/publications/scssr24/ - * Wu, X., Xu, Y., & Wang, L. (2024). Stereo-consistent Screen Space Reflection. Computer Graphics Forum, 43(4). - * - * @param[in] monoUV Mono UV coordinates with depth in Z, [0-1]. Must not be dynamic resolution adjusted. - * @param[in] eyeIndex Index of the originating eye (0 or 1). - * @param[out] sampleUV Mono UV that should be used for sampling (may be in the other eye). - * @param[out] sampleEyeIndex Eye index that owns sampleUV. - */ - void ResolveMonoUVForEye(float3 monoUV, uint eyeIndex, out float2 sampleUV, out uint sampleEyeIndex) - { - sampleUV = monoUV.xy; - sampleEyeIndex = eyeIndex; -# ifdef VR - if (FrameBuffer::IsOutsideFrame(monoUV.xy, false)) { - float3 otherEyeUV = ConvertMonoUVToOtherEye(monoUV, eyeIndex); - if (!FrameBuffer::IsOutsideFrame(otherEyeUV.xy, false)) { - sampleUV = otherEyeUV.xy; - sampleEyeIndex = 1 - eyeIndex; - } - } -# endif - } - -# ifdef VR - /** - * @brief Adjusts UV coordinates for VR stereo rendering when transitioning between eyes or handling boundary conditions. - * - * This function is used in raymarching to check the next UV coordinate. It checks if the current UV coordinates are outside - * the frame. If so, it transitions the UV coordinates to the other eye and adjusts them if they are within the frame of the other eye. - * If the UV coordinates are outside the frame of both eyes, it returns the adjusted UV coordinates for the current eye. - * - * The function ensures that the UV coordinates are correctly adjusted for stereo rendering, taking into account boundary conditions - * and preserving accurate reflections. - * Based on concepts from https://cuteloong.github.io/publications/scssr24/ - * Wu, X., Xu, Y., & Wang, L. (2024). Stereo-consistent Screen Space Reflection. Computer Graphics Forum, 43(4). - * - * We do not have a backface depth so we may be ray marching even though the ray is in an object. - - * @param[in] monoUV Current UV coordinates with depth information, [0-1]. Must not be dynamic resolution adjusted. - * @param[in] eyeIndex Index of the current eye (0 or 1). - * @param[out] fromOtherEye Boolean indicating if the result UV coordinates are from the other eye. - * - * @return Adjusted stereo UV coordinates for rendering, [0-1]. Must be dynamic resolution adjusted later. - */ - float3 ConvertStereoRayMarchUV(float3 monoUV, uint eyeIndex, out bool fromOtherEye) - { - float2 resolvedUV; - uint resolvedEye; - ResolveMonoUVForEye(monoUV, eyeIndex, resolvedUV, resolvedEye); - fromOtherEye = (resolvedEye != eyeIndex); - return ConvertToStereoUV(float3(resolvedUV, monoUV.z), resolvedEye); - } - - /** - * @brief Converts stereo UV coordinates from one eye to the corresponding stereo UV coordinates of the other eye. - * - * This function is used to transition UV coordinates from one eye's perspective to the other eye in a stereo rendering setup. - * It works by converting the stereo UV to mono UV, then to clip space, transforming it into view space, and then reprojecting it into the other eye's - * clip space before converting back to stereo UV coordinates. It also supports dynamic resolution. - * - * @param[in] stereoUV The UV coordinates and depth value (Z component) for the current eye, in the range [0,1]. - * @param[in] eyeIndex Index of the current eye (0 or 1). - * @param[in] dynamicres Optional flag indicating whether dynamic resolution is applied. Default is false. - * @return UV coordinates adjusted to the other eye, with depth. - */ - float3 ConvertStereoUVToOtherEyeStereoUV(float3 stereoUV, uint eyeIndex, bool dynamicres = false) - { - // Convert from dynamic res to true UV space - if (dynamicres) - stereoUV.xy *= FrameBuffer::DynamicResolutionParams2.xy; - - stereoUV.xy = ConvertFromStereoUV(stereoUV.xy, eyeIndex); - stereoUV.xyz = ConvertMonoUVToOtherEye(stereoUV.xyz, eyeIndex); - stereoUV.xy = ConvertToStereoUV(stereoUV.xy, 1 - eyeIndex); - - // Convert back to dynamic res space if necessary - if (dynamicres) - stereoUV.xy *= FrameBuffer::DynamicResolutionParams1.xy; - return stereoUV; - } - - /** - * @brief Returns a smooth fade factor for UVs near the edge of the frame. - * - * This helps avoid abrupt transitions when one eye's SSGI is out of frame or occluded. - * Fade width is tunable; 0.02 is 2% of the frame. - */ - float IsOutsideFrameFade(float2 uv, bool dynamicres = false) - { - float2 max = dynamicres ? FrameBuffer::DynamicResolutionParams1.xy : float2(1, 1); - float2 min = float2(0, 0); - float fadeWidth = 0.02; - float edgeFade = 1.0; - edgeFade *= smoothstep(min.x, min.x + fadeWidth, uv.x); - edgeFade *= smoothstep(max.x, max.x - fadeWidth, uv.x); - edgeFade *= smoothstep(min.y, min.y + fadeWidth, uv.y); - edgeFade *= smoothstep(max.y, max.y - fadeWidth, uv.y); - return edgeFade; - } - - /** - * @brief Blends color data from two eyes based on their UV coordinates and validity. - * - * This function checks the validity of the colors based on their UV coordinates and - * alpha values. It blends the colors while ensuring proper handling of transparency. - * If one eye sees the first person model (depth < VR_FP_Z) and the other sees world geometry (depth > VR_FP_Z), - * the first person model's color is dropped from the blend to avoid outlines. - * - * @param uv1 UV coordinates for the first eye. - * @param color1 Color from the first eye. - * @param uv2 UV coordinates for the second eye. - * @param color2 Color from the second eye. - * @param dynamicres Whether the uvs have dynamic resolution applied - * @return Blended color, including the maximum alpha from both inputs. - */ - float4 BlendEyeColors( - float3 uv1, - float4 color1, - float3 uv2, - float4 color2, - bool dynamicres = false) - { - // Use smooth fade at edge for each eye - float fade1 = IsOutsideFrameFade(uv1.xy, dynamicres); - float fade2 = IsOutsideFrameFade(uv2.xy, dynamicres); - - // Stereo-consistent edge fade: use maximum fade so either eye can keep color if in bounds - float edgeFade = max(fade1, fade2); - - // Occlusion-aware confidence based on depth difference - float depthDiff = abs(uv1.z - uv2.z); - float confidence = 1.0 - smoothstep(0.01, 0.05, depthDiff); - - // Soft first person model mask: fade out FP model near threshold - float fp_fade1 = 1.0 - smoothstep(VR_FP_Z - 1.0, VR_FP_Z + 1.0, uv1.z); // fades from 1 to 0 as depth crosses VR_FP_Z - float fp_fade2 = 1.0 - smoothstep(VR_FP_Z - 1.0, VR_FP_Z + 1.0, uv2.z); - - // If one eye is world and the other is FP, fade out FP smoothly - bool eye1_is_fp = uv1.z < VR_FP_Z; - bool eye2_is_fp = uv2.z < VR_FP_Z; - bool eyes_disagree = eye1_is_fp != eye2_is_fp; - if (eyes_disagree) { - if (eye1_is_fp) - fade1 *= fp_fade1; - if (eye2_is_fp) - fade2 *= fp_fade2; - } - - fade1 *= confidence * edgeFade; - fade2 *= confidence * edgeFade; - - float totalFade = fade1 + fade2 + 1e-5; - float4 blendedColor = (color1 * fade1 + color2 * fade2) / totalFade; - blendedColor.a = max(color1.a * fade1, color2.a * fade2); - return blendedColor; - } - - float4 BlendEyeColors(float2 uv1, float4 color1, float2 uv2, float4 color2, bool dynamicres = false) - { - return BlendEyeColors(float3(uv1, 0), color1, float3(uv2, 0), color2, dynamicres); - } - - /** - * @brief Result of a stereo bilateral reprojection: other-eye pixel coords and blend weight. - */ - struct StereoBilateralResult - { - float2 otherStereoUV; ///< Stereo UV in the other eye - int2 otherPx; ///< Pixel coordinate in the other eye - float blendWeight; ///< [0, maxBlend] bilateral blend weight - bool valid; ///< True if reprojection succeeded - bool backCheckPassed; ///< True if round-trip reprojection validated - }; - - /** - * @brief Reprojects a pixel to the other eye and computes pixel coordinates. - * - * First stage of the stereo bilateral filter from Shi, Billeter, Eisemann 2022. - * Returns the other-eye pixel location; the caller must sample depth at that - * location and call FinalizeStereoBlend to complete the bilateral weight. - * - * @param[in] stereoUV Stereo UV of the source pixel [0,1] - * @param[in] depth Depth at the source pixel - * @param[in] eyeIndex Eye index of the source pixel (0 or 1) - * @param[in] frameDim Dimensions of the buffer (for pixel coord conversion) - * @return StereoBilateralResult with valid=false if reprojection is out of bounds. - */ - StereoBilateralResult ReprojectToOtherEye( - float2 stereoUV, - float depth, - uint eyeIndex, - float2 frameDim) - { - StereoBilateralResult result; - result.otherStereoUV = 0; - result.otherPx = int2(0, 0); - result.blendWeight = 0; - result.valid = false; - result.backCheckPassed = false; - - uint otherEyeIndex = 1 - eyeIndex; - - float2 monoUV = ConvertFromStereoUV(stereoUV, eyeIndex); - float3 otherEyeUV = ConvertMonoUVToOtherEye(float3(monoUV, depth), eyeIndex); - - if (FrameBuffer::IsOutsideFrame(otherEyeUV.xy, false)) - return result; - - result.otherStereoUV = ConvertToStereoUV(otherEyeUV.xy, otherEyeIndex); - result.otherPx = clamp(int2(result.otherStereoUV * frameDim), int2(0, 0), int2(frameDim) - 1); - result.valid = true; - return result; - } - - /** - * @brief Computes bilateral blend weight with depth comparison and back-check. - * - * Second stage of the stereo bilateral filter from Shi, Billeter, Eisemann 2022. - * Compares the sampled depth at the other eye's pixel against the expected depth, - * and performs the back-check (round-trip reprojection validation). - * - * @param[in,out] result Result from ReprojectToOtherEye; blendWeight and backCheckPassed are filled in. - * @param[in] stereoUV Stereo UV of the source pixel (same as passed to ReprojectToOtherEye) - * @param[in] depth Depth at the source pixel - * @param[in] otherEyeDepth Actual depth sampled at the other eye's pixel - * @param[in] eyeIndex Eye index of the source pixel - * @param[in] frameDim Dimensions of the buffer - * @param[in] depthSigma Gaussian sigma for bilateral depth weight - * @param[in] maxBlend Maximum blend factor - * @param[in] backCheckThreshold Max pixel distance for back-check (0 to disable). Default 8.0. - */ - void FinalizeStereoBlend( - inout StereoBilateralResult result, - float2 stereoUV, - float depth, - float otherEyeDepth, - uint eyeIndex, - float2 frameDim, - float depthSigma, - float maxBlend, - float backCheckThreshold = 8.0) - { - // Bilateral weight: compare sampled depth at other eye against source depth - float depthDiff = abs(depth - otherEyeDepth); - float depthWeight = exp(-depthDiff * depthDiff / (depthSigma * depthSigma + 1e-8)); - - // Back-check: reproject Q (in eye B) back to eye A and verify round-trip. - // Two VP matrix multiplications accumulate float32 error (~3-5px at medium range), - // so the threshold must be generous enough to pass valid surfaces while catching - // true occlusion discontinuities (which produce errors of tens to hundreds of pixels). - uint otherEyeIndex = 1 - eyeIndex; - result.backCheckPassed = true; - if (backCheckThreshold > 0) { - float2 otherMonoUV = ConvertFromStereoUV(result.otherStereoUV, otherEyeIndex); - float3 roundTripUV = ConvertMonoUVToOtherEye(float3(otherMonoUV, otherEyeDepth), otherEyeIndex); - float2 roundTripStereoUV = ConvertToStereoUV(roundTripUV.xy, eyeIndex); - float2 pixelDist = abs(roundTripStereoUV * frameDim - (stereoUV * frameDim)); - // Use max component so a large error in either axis triggers the check - result.backCheckPassed = max(pixelDist.x, pixelDist.y) < backCheckThreshold; - if (!result.backCheckPassed) - depthWeight *= 0.1; // Heavily penalize but don't fully reject - } - - result.blendWeight = depthWeight * maxBlend; - } -# endif // VR -#endif // PSHADER - -#ifdef VSHADER - struct VR_OUTPUT - { - float4 VRPosition; - float ClipDistance; - float CullDistance; - }; - - /** - Gets the eyeIndex for VSHADER - @returns eyeIndex (0 left, 1 right) - */ - uint GetEyeIndexVS(uint instanceID = 0) - { -# ifdef VR - return StereoEnabled * (instanceID & 1); -# endif // VR - return 0; - } - - /** - Gets VR Output - @param clipPos clipPosition. Typically the VSHADER position at SV_POSITION0 - @param a_eyeIndex The eyeIndex; 0 is left, 1 is right - @returns VR_OUTPUT with VR values - */ - VR_OUTPUT GetVRVSOutput(float4 clipPos, uint a_eyeIndex = 0) - { - VR_OUTPUT vsout = { - 0.0.xxxx, // VRPosition - 0.0f, // ClipDistance - 0.0f // CullDistance - }; - -# ifdef VR - bool isStereoEnabled = (StereoEnabled != 0); - float2 clipEdges; - - if (isStereoEnabled) { - clipEdges.x = dot(clipPos, EyeClipEdge[a_eyeIndex]); - clipEdges.y = clipEdges.x; // Both use the same calculation - } else { - clipEdges = float2(1.0f, 1.0f); - } - - float stereoAdjustment = 2.0f - StereoEnabled; - float eyeOffset = dot(EyeOffsetScale, Math::IdentityMatrix[a_eyeIndex].xy); - - float xPositionOffset = eyeOffset * clipPos.w * (isStereoEnabled ? 1.0f : 0.0f); - float xPositionBase = stereoAdjustment * clipPos.x; - - vsout.VRPosition.x = xPositionBase * 0.5f + xPositionOffset; - vsout.VRPosition.y = clipPos.y; - vsout.VRPosition.z = clipPos.z; - vsout.VRPosition.w = clipPos.w; - - vsout.ClipDistance = clipEdges.y; - vsout.CullDistance = clipEdges.x; -# endif // VR - return vsout; - } -#endif - -} -#endif //__VR_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index cadbf90424..9075bfc0ba 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -7,8 +7,6 @@ #include "Common/Shading.hlsli" #include "Common/SharedData.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" -#include "Common/VR.hlsli" - Texture2D SpecularTexture : register(t0); Texture2D AlbedoTexture : register(t1); Texture2D NormalRoughnessTexture : register(t2); @@ -27,11 +25,6 @@ Texture2D DepthTexture : register(t4); Texture2D DepthTexture : register(t4); #endif -#if defined(VR_STEREO_OPT) -# include "VRStereoOptimizations/modes.hlsli" -Texture2D StereoOptModeTexture : register(t16); -#endif - #if defined(DYNAMIC_CUBEMAPS) Texture2D ReflectanceTexture : register(t5); TextureCube EnvTexture : register(t6); @@ -101,19 +94,6 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float2 uv = float2(dispatchID.xy + 0.5) * SharedData::BufferDim.zw; uv *= FrameBuffer::DynamicResolutionParams2.xy; // adjust for dynamic res - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - -#if defined(VR_STEREO_OPT) - if (eyeIndex == 1) { - uint mode = StereoOptModeTexture[uint2(dispatchID.xy)] & 0x0F; - if (mode == MODE_MAIN) { // stencil-culled in Eye 1, filled by ReprojectionCS - return; - } - } -#endif - - uv = Stereo::ConvertFromStereoUV(uv, eyeIndex); - float3 normalGlossiness = NormalRoughnessTexture[dispatchID.xy]; float3 normalVS = GBuffer::DecodeNormal(normalGlossiness.xy); @@ -123,16 +103,16 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float depth = DepthTexture[dispatchID.xy]; float4 positionWS = float4(2 * float2(uv.x, -uv.y + 1) - 1, depth, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; if (depth == 1.0) - MotionVectorsRW[dispatchID.xy] = MotionBlur::GetSSMotionVector(positionWS, positionWS, eyeIndex); // Apply sky motion vectors + MotionVectorsRW[dispatchID.xy] = MotionBlur::GetSSMotionVector(positionWS, positionWS); // Apply sky motion vectors float glossiness = normalGlossiness.z; float3 linDiffuseColor = Color::IrradianceToLinear(diffuseColor); - float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); + float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(normalVS, 0)).xyz); #if defined(SSGI) @@ -155,11 +135,7 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float3 vanillaDALC = Color::Ambient(max(0, SharedData::GetAmbient(normalWS))); # if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMS = positionWS.xyz; -# endif sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, normalWS); float skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, normalWS); directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(vanillaDALC, -normalWS, skylightingDiffuse) * albedo; @@ -224,11 +200,7 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float directionalAmbientColorSpecular = Color::RGBToLuminance(Color::Ambient(max(0, SharedData::GetAmbient(R)))) * Color::ReflectionNormalisationScale; # if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMS = positionWS.xyz; -# endif sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, R); float skylightingSpecular = Skylighting::EvaluateSpecular(skylightingSH, specularLobe); @@ -322,10 +294,6 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, #if defined(DEBUG) -# if defined(VR) - uv.x += (eyeIndex ? 0.1 : -0.1); -# endif // VR - if (uv.x < 0.5 && uv.y < 0.5) { color = color; } else if (uv.x < 0.5) { diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index 3e510142a2..70708e733a 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -5,8 +5,6 @@ #include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" - #if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) # undef IBL #endif @@ -19,9 +17,6 @@ struct VS_INPUT float4 InstanceData2: TEXCOORD5; float4 InstanceData3: TEXCOORD6; float4 InstanceData4: TEXCOORD7; -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -37,11 +32,6 @@ struct VS_OUTPUT #endif // RENDER_DEPTH float4 ViewPosition: POSITION3; -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 - uint EyeIndex: EYEIDX0; -#endif // VR }; #ifdef VSHADER @@ -52,25 +42,14 @@ cbuffer PerTechnique : register(b0) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float4x4 WorldViewProj[1] : packoffset(c0); - row_major float4x4 World[1] : packoffset(c4); - row_major float4x4 PreviousWorld[1] : packoffset(c8); -# else - row_major float4x4 WorldViewProj[2] : packoffset(c0); - row_major float4x4 World[2] : packoffset(c8); - row_major float4x4 PreviousWorld[2] : packoffset(c16); -# endif // !VR + row_major float4x4 WorldViewProj : packoffset(c0); + row_major float4x4 World : packoffset(c4); + row_major float4x4 PreviousWorld : packoffset(c8); }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout = (VS_OUTPUT)0; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif // VR - ); float3 scaledModelPosition = input.InstanceData1.www * input.Position.xyz; float3 adjustedModelPosition = 0.0.xxx; @@ -78,28 +57,20 @@ VS_OUTPUT main(VS_INPUT input) adjustedModelPosition.y = dot(input.InstanceData2.yx, scaledModelPosition.xy); adjustedModelPosition.z = scaledModelPosition.z; float4 finalModelPosition = float4(input.InstanceData1.xyz + adjustedModelPosition.xyz, 1.0); - float4 viewPosition = mul(WorldViewProj[eyeIndex], finalModelPosition); + float4 viewPosition = mul(WorldViewProj, finalModelPosition); # ifdef RENDER_DEPTH vsout.Depth.xy = viewPosition.zw; vsout.Depth.zw = input.InstanceData2.zw; # else - vsout.WorldPosition = mul(World[eyeIndex], finalModelPosition); - vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], finalModelPosition); + vsout.WorldPosition = mul(World, finalModelPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld, finalModelPosition); vsout.ViewPosition = viewPosition; # endif // RENDER_DEPTH vsout.Position = viewPosition; vsout.TexCoord = float3(input.TexCoord0.xy, FogParam.z); -# ifdef VR - vsout.EyeIndex = eyeIndex; - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); - vsout.Position = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR - return vsout; } #endif // VSHADER @@ -125,12 +96,10 @@ SamplerState SampDiffuse : register(s0); Texture2D TexDiffuse : register(t0); -# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif // !VR cbuffer PerFrame : register(b12) { @@ -182,10 +151,10 @@ const static float DepthOffsets[16] = { # include "Common/ShadowSampling.hlsli" # if defined(EXP_HEIGHT_FOG) -void ApplyReflectionExponentialHeightFog(inout float3 color, float3 positionWS, float4 screenPosition, uint eyeIndex) +void ApplyReflectionExponentialHeightFog(inout float3 color, float3 positionWS, float4 screenPosition) { float3 fogColor = Color::Fog(AmbientColor.xyz); - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(screenPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, screenPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(positionWS, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(screenPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, screenPosition.z, 1)); color = lerp(color, exponentialHeightFog.xyz, exponentialHeightFog.w); } # endif @@ -194,11 +163,6 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; -# if !defined(VR) - uint eyeIndex = 0; -# else - uint eyeIndex = input.EyeIndex; -# endif // !VR # if defined(EXP_HEIGHT_FOG) const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; # endif @@ -231,25 +195,25 @@ PS_OUTPUT main(PS_INPUT input) } # if defined(DEFERRED) - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); float dirShadow = 1; # if defined(SCREEN_SPACE_SHADOWS) - dirShadow = lerp(1.0, ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise, eyeIndex), 0.8); + dirShadow = lerp(1.0, ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise), 0.8); # endif if (dirShadow != 0.0) - dirShadow *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + dirShadow *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); float llDirLightMult = (SharedData::linearLightingSettings.enableLinearLighting && !SharedData::linearLightingSettings.isDirLightLinear) ? SharedData::linearLightingSettings.dirLightMult : 1.0f; float3 diffuseColor = Color::DirectionalLight(SharedData::DirLightColor.xyz / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * dirShadow * 0.5 * llDirLightMult * Color::VanillaNormalization(); # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif @@ -270,26 +234,26 @@ PS_OUTPUT main(PS_INPUT input) # if defined(EXP_HEIGHT_FOG) if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { - ApplyReflectionExponentialHeightFog(psout.Diffuse.xyz, input.WorldPosition.xyz, input.Position, eyeIndex); + ApplyReflectionExponentialHeightFog(psout.Diffuse.xyz, input.WorldPosition.xyz, input.Position); } # endif - psout.MotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + psout.MotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); - psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false, eyeIndex)); + psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false)); psout.Normal.zw = 0; psout.Albedo = float4(baseColor.xyz, 1); psout.Masks = float4(0, 0, 1, 0); # else - float dirShadow = ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + float dirShadow = ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); float llDirLightMult = (SharedData::linearLightingSettings.enableLinearLighting && !SharedData::linearLightingSettings.isDirLightLinear) ? SharedData::linearLightingSettings.dirLightMult : 1.0f; float3 diffuseColor = Color::DirectionalLight(SharedData::DirLightColor.xyz / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * dirShadow * 0.5 * llDirLightMult * Color::VanillaNormalization(); # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif @@ -308,7 +272,7 @@ PS_OUTPUT main(PS_INPUT input) float3 color = diffuseColor * baseColor.xyz; # if defined(EXP_HEIGHT_FOG) if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { - ApplyReflectionExponentialHeightFog(color, input.WorldPosition.xyz, input.Position, eyeIndex); + ApplyReflectionExponentialHeightFog(color, input.WorldPosition.xyz, input.Position); } # endif psout.Diffuse = float4(color, 1.0); diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index 2094160a73..1f641a143f 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -7,8 +7,6 @@ #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" -#include "Common/VR.hlsli" - #define EFFECT #if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) @@ -39,9 +37,6 @@ struct VS_INPUT float4 BoneWeights: BLENDWEIGHT0; float4 BoneIndices: BLENDINDICES0; #endif -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -87,35 +82,19 @@ struct VS_OUTPUT float3 ScreenSpaceNormal: TEXCOORD7; # endif #endif -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 - uint EyeIndex: EYEIDX0; -#endif // VR }; #ifdef VSHADER cbuffer VS_PerFrame : register(b12) { -# if !defined(VR) - row_major float4x4 ScreenProj[1] : packoffset(c0); - row_major float4x4 ViewProj[1] : packoffset(c8); -# if defined(SKINNED) - float3 BonesPivot[1] : packoffset(c40); -# if defined(MOTIONVECTORS_NORMALS) - float3 PreviousBonesPivot[1] : packoffset(c41); -# endif // MOTIONVECTORS_NORMALS -# endif // SKINNED -# else - row_major float4x4 ScreenProj[2] : packoffset(c0); - row_major float4x4 ViewProj[2] : packoffset(c16); -# if defined(SKINNED) - float3 BonesPivot[2] : packoffset(c80); -# if defined(MOTIONVECTORS_NORMALS) - float3 PreviousBonesPivot[2] : packoffset(c82); -# endif // MOTIONVECTORS_NORMALS -# endif // SKINNED -# endif // VR + row_major float4x4 ScreenProj : packoffset(c0); + row_major float4x4 ViewProj : packoffset(c8); +# if defined(SKINNED) + float3 BonesPivot : packoffset(c40); +# if defined(MOTIONVECTORS_NORMALS) + float3 PreviousBonesPivot : packoffset(c41); +# endif // MOTIONVECTORS_NORMALS +# endif // SKINNED }; cbuffer PerTechnique : register(b0) @@ -134,21 +113,12 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float3x4 World[1] : packoffset(c0); - row_major float3x4 PreviousWorld[1] : packoffset(c3); + row_major float3x4 World : packoffset(c0); + row_major float3x4 PreviousWorld : packoffset(c3); float4 MatProj[3] : packoffset(c6); - float4 EyePosition[1] : packoffset(c12); - float4 PosAdjust[1] : packoffset(c13); + float4 EyePosition : packoffset(c12); + float4 PosAdjust : packoffset(c13); float4 TexcoordOffsetMembrane : packoffset(c14); -# else - row_major float3x4 World[2] : packoffset(c0); - row_major float3x4 PreviousWorld[2] : packoffset(c6); - float4 MatProj[3] : packoffset(c12); - float4 EyePosition[2] : packoffset(c21); - float4 PosAdjust[2] : packoffset(c23); - float4 TexcoordOffsetMembrane : packoffset(c25); -# endif // VR } cbuffer IndexedTexcoordBuffer : register(b11) @@ -190,47 +160,42 @@ float GetProjectedU(float3 worldPosition, float4 texCoordOffset) return abs(0.318309158 * projUvTmp4) * texCoordOffset.w + texCoordOffset.y; } -float GetProjectedV(float3 worldPosition, uint a_eyeIndex = 0) +float GetProjectedV(float3 worldPosition) { - return (-PosAdjust[a_eyeIndex].x + (PosAdjust[a_eyeIndex].z + worldPosition.z)) / PosAdjust[a_eyeIndex].y; + return (-PosAdjust.x + (PosAdjust.z + worldPosition.z)) / PosAdjust.y; } # endif VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif // VR - ); precise float4 inputPosition = float4(input.Position.xyz, 1.0); - precise row_major float4x4 world4x4 = float4x4(World[eyeIndex][0], World[eyeIndex][1], World[eyeIndex][2], float4(0, 0, 0, 1)); + precise row_major float4x4 world4x4 = float4x4(World[0], World[1], World[2], float4(0, 0, 0, 1)); precise float3x3 world3x3 = - transpose(float3x3(transpose(World[eyeIndex])[0], transpose(World[eyeIndex])[1], transpose(World[eyeIndex])[2])); + transpose(float3x3(transpose(World)[0], transpose(World)[1], transpose(World)[2])); # if defined(SKY_OBJECT) - float4x4 viewProj = float4x4(ViewProj[eyeIndex][0], ViewProj[eyeIndex][1], ViewProj[eyeIndex][3], ViewProj[eyeIndex][3]); + float4x4 viewProj = float4x4(ViewProj[0], ViewProj[1], ViewProj[3], ViewProj[3]); # else - row_major float4x4 viewProj = ViewProj[eyeIndex]; + row_major float4x4 viewProj = ViewProj; # endif # if defined(SKINNED) precise int4 actualIndices = 765.01.xxxx * input.BoneIndices.xyzw; # if defined(MOTIONVECTORS_NORMALS) float3x4 previousBoneTransformMatrix = - Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot[eyeIndex], input.BoneWeights); + Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot, input.BoneWeights); precise float4 previousWorldPosition = float4(mul(inputPosition, transpose(previousBoneTransformMatrix)), 1); # endif float3x4 boneTransformMatrix = - Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot[eyeIndex], input.BoneWeights); + Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot, input.BoneWeights); precise float4 worldPosition = float4(mul(inputPosition, transpose(boneTransformMatrix)), 1); float4 viewPos = mul(viewProj, worldPosition); # else - precise float4 worldPosition = float4(mul(World[eyeIndex], inputPosition), 1); - precise float4 previousWorldPosition = float4(mul(PreviousWorld[eyeIndex], inputPosition), 1); + precise float4 worldPosition = float4(mul(World, inputPosition), 1); + precise float4 previousWorldPosition = float4(mul(PreviousWorld, inputPosition), 1); precise row_major float4x4 modelView = mul(viewProj, world4x4); float4 viewPos = mul(modelView, inputPosition); # endif @@ -299,7 +264,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(NORMALS) && !defined(MEMBRANE) texCoord.y = dot(MatProj[1].xyz, inputPosition.xyz); # else - texCoord.y = GetProjectedV(worldPosition.xyz, eyeIndex); + texCoord.y = GetProjectedV(worldPosition.xyz); # endif # else # if defined(TEXTURE) @@ -332,7 +297,7 @@ VS_OUTPUT main(VS_INPUT input) float3 eyePosition = 0.0.xxx; # if defined(MEMBRANE) && defined(TEXTURE) && !defined(SKINNED) - eyePosition = EyePosition[eyeIndex].xyz; + eyePosition = EyePosition.xyz; # endif float3 viewPosition = inputPosition.xyz; @@ -375,7 +340,7 @@ VS_OUTPUT main(VS_INPUT input) # elif defined(FALLOFF) || (defined(SKINNED) && defined(MEMBRANE)) float3 screenSpaceNormal = worldNormal; # else - float4x4 modelScreen = mul(ScreenProj[eyeIndex], world4x4); + float4x4 modelScreen = mul(ScreenProj, world4x4); float3 screenSpaceNormal = normalize(mul(modelScreen, float4(normal, 0))).xyz; # endif @@ -397,13 +362,6 @@ VS_OUTPUT main(VS_INPUT input) vsout.PreviousWorldPosition = previousWorldPosition; # endif -# ifdef VR - vsout.EyeIndex = eyeIndex; - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); - vsout.Position = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR return vsout; } #endif @@ -451,12 +409,10 @@ struct PS_OUTPUT #ifdef PSHADER -# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif // !VR cbuffer PerTechnique : register(b0) { @@ -474,7 +430,6 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) float4 PLightPositionX[1] : packoffset(c0); float4 PLightPositionY[1] : packoffset(c1); float4 PLightPositionZ[1] : packoffset(c2); @@ -487,20 +442,6 @@ cbuffer PerGeometry : register(b2) float4 AlphaTestRef : packoffset(c9); float4 MembraneRimColor : packoffset(c10); float4 MembraneVars : packoffset(c11); -# else - float4 PLightPositionX[2] : packoffset(c0); - float4 PLightPositionY[2] : packoffset(c2); - float4 PLightPositionZ[2] : packoffset(c4); - float4 PLightingRadiusInverseSquared : packoffset(c6); - float4 PLightColorR : packoffset(c7); - float4 PLightColorG : packoffset(c8); - float4 PLightColorB : packoffset(c9); - float4 DLightColor : packoffset(c10); - float4 PropertyColor : packoffset(c11); // VR should be 11; this could start earlier though - float4 AlphaTestRef : packoffset(c12); - float4 MembraneRimColor : packoffset(c13); - float4 MembraneVars : packoffset(c14); -# endif }; # if defined(LIGHT_LIMIT_FIX) @@ -566,7 +507,7 @@ void ExtractEffectLighting(float3 inputColor, out float3 dirColor, out float3 am } # if defined(LIGHTING) -float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPosition, uint eyeIndex, inout float shadowVariance) +float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPosition, inout float shadowVariance) { float3 color = DLightColor.xyz * Color::EffectLightingMult(); bool suppressExternalEmittance = SharedData::InInterior && (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::SuppressExternalEmittance); @@ -577,11 +518,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo # if defined(SKYLIGHTING) float skylightingDiffuse = 1.0; if (!SharedData::InInterior) { -# if defined(VR) - float3 positionMSSkylight = worldPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMSSkylight = worldPosition; -# endif sh2 skylightingSH = Skylighting::SampleNoBias(positionMSSkylight); skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, float3(0, 0, 1), Skylighting::GetFadeOutFactor(positionMSSkylight)); @@ -604,7 +541,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); if (inWorld && ShadowSampling::HasDirectionalShadows()) - dirShadow = ShadowSampling::Get3DFilteredShadow(worldPosition.xyz, viewDirection, screenPosition, eyeIndex, unusedSurfaceShadow); + dirShadow = ShadowSampling::Get3DFilteredShadow(worldPosition.xyz, viewDirection, screenPosition, unusedSurfaceShadow); shadowVariance = 1.0 - sqrt(saturate(fwidth(dirShadow))); @@ -612,7 +549,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif @@ -633,7 +570,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld)) # endif { - float4 lightDistanceSquared = (PLightPositionX[eyeIndex] - msPosition.xxxx) * (PLightPositionX[eyeIndex] - msPosition.xxxx) + (PLightPositionY[eyeIndex] - msPosition.yyyy) * (PLightPositionY[eyeIndex] - msPosition.yyyy) + (PLightPositionZ[eyeIndex] - msPosition.zzzz) * (PLightPositionZ[eyeIndex] - msPosition.zzzz); + float4 lightDistanceSquared = (PLightPositionX[0] - msPosition.xxxx) * (PLightPositionX[0] - msPosition.xxxx) + (PLightPositionY[0] - msPosition.yyyy) * (PLightPositionY[0] - msPosition.yyyy) + (PLightPositionZ[0] - msPosition.zzzz) * (PLightPositionZ[0] - msPosition.zzzz); float4 lightFadeMul = 1.0.xxxx - saturate(PLightingRadiusInverseSquared * lightDistanceSquared); color.x += dot(Color::PointLight(PLightColorR.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); color.y += dot(Color::PointLight(PLightColorG.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); @@ -643,7 +580,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo return color; } # else -float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPosition, float depth, uint eyeIndex, inout float shadowVariance) +float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPosition, float depth, inout float shadowVariance) { float3 dirColor; float3 ambientColor; @@ -679,7 +616,7 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi for (uint i = 0; i < sampleCount; i++) { float t = (float(i) + noise) * rcpSampleCount; float3 samplePositionWS = lerp(startPosition, endPosition, t); - shadow += ShadowSampling::GetWorldShadow(samplePositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + shadow += ShadowSampling::GetWorldShadow(samplePositionWS, FrameBuffer::CameraPosAdjust.xyz); } shadow *= rcpSampleCount; } @@ -690,7 +627,7 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif @@ -702,12 +639,6 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout = (PS_OUTPUT)0; -# if !defined(VR) - uint eyeIndex = 0; -# else - uint eyeIndex = input.EyeIndex; -# endif // !VR - float4 fogMul = 1; # if !defined(MULTBLEND) fogMul.xyz = input.FogAlpha; @@ -757,13 +688,13 @@ PS_OUTPUT main(PS_INPUT input) float shadowVariance = 1.0; # if defined(LIGHTING) - propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, eyeIndex, shadowVariance); + propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, shadowVariance); # if defined(LIGHT_LIMIT_FIX) uint lightCount = 0; - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); bool inWorld = Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld; uint clusterIndex = 0; @@ -777,7 +708,7 @@ PS_OUTPUT main(PS_INPUT input) if (LightLimitFix::IsLightIgnored(light) || light.lightFlags & LightLimitFix::LightFlags::Shadow) { continue; } - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -885,7 +816,7 @@ PS_OUTPUT main(PS_INPUT input) # if !defined(LIGHTING) && defined(VC) && defined(TEXCOORD) && defined(NORMALS) && defined(TEXTURE) && defined(FALLOFF) && defined(SOFT) if (Permutation::PixelShaderDescriptor & Permutation::EffectFlags::GrayscaleToAlpha && lightingInfluence == 1.0) - lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, eyeIndex, shadowVariance); + lightColor = GetLightingShadow(lightColor, input.WorldPosition.xyz, input.Position.xy, depth, shadowVariance); # endif lightColor = Color::EffectMult(lightColor); @@ -903,7 +834,7 @@ PS_OUTPUT main(PS_INPUT input) float3 vanillaFogColor = fogColor; float expFogFactor = 0; if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); expFogFactor = exponentialHeightFog.w; # if defined(ADDBLEND) || defined(MULTBLEND) || defined(MULTBLEND_DECAL) fogColor = exponentialHeightFog.xyz; @@ -974,7 +905,7 @@ PS_OUTPUT main(PS_INPUT input) float3 screenSpaceNormal = normalize(input.ScreenSpaceNormal); # endif psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), 0.0, psout.Diffuse.w); - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); psout.MotionVectors = float4(screenMotionVector, 0.0, psout.Diffuse.w); # endif @@ -991,7 +922,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # elif defined(MOTIONVECTORS_NORMALS) - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); psout.MotionVectors = screenMotionVector; # if (defined(MEMBRANE) && defined(SKINNED) && defined(NORMALS)) diff --git a/package/Shaders/ISApplyVolumetricLighting.hlsl b/package/Shaders/ISApplyVolumetricLighting.hlsl index 6d42ead969..aa5d9d0dd1 100644 --- a/package/Shaders/ISApplyVolumetricLighting.hlsl +++ b/package/Shaders/ISApplyVolumetricLighting.hlsl @@ -1,6 +1,5 @@ #include "Common/DummyVSTexCoord.hlsl" #include "Common/FrameBuffer.hlsli" -#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -39,12 +38,6 @@ PS_OUTPUT main(PS_INPUT input) float2 screenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); float depth = DepthTex.Sample(DepthSampler, screenPosition).x; -# ifdef VR - if (depth < 0.0001) { // not a valid location - psout.VL = 0.0; - return psout; - } -# endif float repartition = clamp(RepartitionTex.SampleLevel(RepartitionSampler, depth, 0).x, 0, 0.9999); float vl = g_IntensityX_TemporalY.x * VLTex.SampleLevel(VLSampler, float3(input.TexCoord, repartition), 0).x; @@ -54,33 +47,11 @@ PS_OUTPUT main(PS_INPUT input) if (0.001 < g_IntensityX_TemporalY.y) { float2 motionVector = MotionVectorsTex.Sample(MotionVectorsSampler, screenPosition).xy; -# ifdef VR - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); - float2 previousTexCoord = Stereo::ConvertFromStereoUV(input.TexCoord, eyeIndex); - previousTexCoord += motionVector; - bool isValid = previousTexCoord.x >= 0 && previousTexCoord.x < 1 && previousTexCoord.y >= 0 && previousTexCoord.y < 1; - previousTexCoord = Stereo::ConvertToStereoUV(previousTexCoord, eyeIndex); -# else float2 previousTexCoord = input.TexCoord + motionVector; bool isValid = previousTexCoord.x >= 0 && previousTexCoord.x < 1 && previousTexCoord.y >= 0 && previousTexCoord.y < 1; -# endif float2 previousScreenPosition = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(previousTexCoord); float previousVl = PreviousFrameTex.Sample(PreviousFrameSampler, previousScreenPosition).x; - float previousDepth = PreviousDepthTex.Sample(PreviousDepthSampler, -# ifndef VR - previousScreenPosition -# else - // In VR with dynamic resolution enabled, there's a bug with the depth stencil. - // The depth stencil from ISDepthBufferCopy is actually full size and not scaled. - // Thus there's never a need to scale it down. - previousTexCoord -# endif - ) - .x; - -# ifdef VR - isValid = isValid && abs(previousDepth) > 0.0001; -# endif + float previousDepth = PreviousDepthTex.Sample(PreviousDepthSampler, previousScreenPosition).x; float temporalContribution = g_IntensityX_TemporalY.y * (1 - smoothstep(0, 1, min(1, 100 * abs(depth - previousDepth)))); psout.VL = lerp(adjustedVl, previousVl, temporalContribution * isValid); diff --git a/package/Shaders/ISFullScreenVR.hlsl b/package/Shaders/ISFullScreenVR.hlsl deleted file mode 100644 index 455602e3c9..0000000000 --- a/package/Shaders/ISFullScreenVR.hlsl +++ /dev/null @@ -1,69 +0,0 @@ -#include "Common/DummyVSTexCoord.hlsl" -#include "Common/VR.hlsli" - -typedef VS_OUTPUT PS_INPUT; - -struct PS_OUTPUT -{ - float4 Color: SV_Target0; // Final color output for the pixel shader. -}; - -#if defined(PSHADER) -SamplerState ImageSampler : register(s0); // Sampler state for texture sampling. -Texture2D ImageTex : register(t0); // Texture to sample colors from. - -cbuffer PerGeometry : register(b2) -{ - float4 FullScreenColor; // Color applied to the final output, used for tinting or blending effects. - float4 Params0; // General parameters; may include scaling or offset values. - float4 Params1; // Length parameters for scaling or thresholding; Params1.z represents `g_flTime`. - float4 UpsampleParams; // Dynamic resolution parameters: - // - UpsampleParams.x: fDynamicResolutionWidthRatio - // - UpsampleParams.y: fDynamicResolutionHeightRatio - // - UpsampleParams.z: fDynamicResolutionPreviousWidthRatio - // - UpsampleParams.w: fDynamicResolutionPreviousHeightRatio -}; - -// Function to generate noise using Valve's ScreenSpaceDither method. -// References: -// - https://blog.frost.kiwi/GLSL-noise-and-radial-gradient/ -// - https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf -float3 ScreenSpaceDither(float2 vScreenPos) -{ - // Iestyn's RGB dither (7 asm instructions) from Portal 2 X360, - // slightly modified for VR applications. - float3 vDither = dot(float2(171.0, 231.0), vScreenPos.xy + Params1.zz).xxx; - vDither.rgb = frac(vDither.rgb / float3(103.0, 71.0, 97.0)) - float3(0.5, 0.5, 0.5); - return (vDither.rgb / 255.0) * 0.375; // Normalize dither values to a suitable range. -} - -PS_OUTPUT main(PS_INPUT input) -{ - PS_OUTPUT psout; - - float2 uv = input.TexCoord; // Get the UV coordinates from input. - - // Convert UV to normalized screen space [-1, 1]. - float2 normalizedScreenCoord = Stereo::ConvertUVToNormalizedScreenSpace(uv); - - // Calculate the length of the normalized screen coordinates. - float normalizedLength = saturate(Params1.x * (length(normalizedScreenCoord) - Params1.y) * Params0.x); - - // Upsample and clamp texture coordinates based on dynamic resolution. - float2 uvScaled = min(UpsampleParams.zw, UpsampleParams.xy * uv.xy); // Clamp UVs to prevent overflow. - float3 sampledColor = ImageTex.Sample(ImageSampler, uvScaled).xyz; // Sample color from the texture. - - // Manipulate the sampled color based on the normalized length. - float3 finalColor = sampledColor * (1.0 + normalizedLength); // Scale sampled color. - - // Generate noise to apply to the final color. - float3 noise = ScreenSpaceDither(input.Position.xy); - finalColor += Params0.yyy * noise * Params1.www; // Adjust final color with noise. - - // Final color manipulation: blend final color with FullScreenColor. - psout.Color.xyz = lerp(finalColor, FullScreenColor.xyz, FullScreenColor.www); // Blend based on the alpha component. - psout.Color.w = 1.0; // Set alpha to full opacity. - - return psout; // Return the pixel shader output. -} -#endif diff --git a/package/Shaders/ISReflectionsRayTracing.hlsl b/package/Shaders/ISReflectionsRayTracing.hlsl index 0f9c45d397..9e7604a6a2 100644 --- a/package/Shaders/ISReflectionsRayTracing.hlsl +++ b/package/Shaders/ISReflectionsRayTracing.hlsl @@ -2,7 +2,6 @@ #include "Common/FrameBuffer.hlsli" #include "Common/MotionBlur.hlsli" #include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -33,20 +32,19 @@ static const int binaryIterations = ceil(log2(iterations)); static const float rayLength = 1.0; -float2 ConvertRaySample(float2 raySample, uint eyeIndex) +float2 ConvertRaySample(float2 raySample) { - return FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(Stereo::ConvertToStereoUV(raySample, eyeIndex)); + return FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(raySample); } -float2 ConvertRaySamplePrevious(float2 raySample, uint eyeIndex) +float2 ConvertRaySamplePrevious(float2 raySample) { - return FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(Stereo::ConvertToStereoUV(raySample, eyeIndex)); + return FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(raySample); } float4 GetReflectionColor( float3 projReflectionDirection, - float3 projPosition, - uint eyeIndex) + float3 projPosition) { float3 prevRaySample; float3 raySample = projPosition; @@ -55,27 +53,24 @@ float4 GetReflectionColor( prevRaySample = raySample; raySample = projPosition + (float(i) / float(iterations)) * projReflectionDirection; - float2 sampleUV; - uint sampleEyeIndex; - Stereo::ResolveMonoUVForEye(raySample, eyeIndex, sampleUV, sampleEyeIndex); + float2 sampleUV = raySample.xy; if (FrameBuffer::IsOutsideFrame(sampleUV)) return 0.0; - float iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV, sampleEyeIndex), 0).x; + float iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV), 0).x; if (saturate((raySample.z - iterationDepth) / SSRParams.y) > 0.0) { float3 binaryMinRaySample = prevRaySample; float3 binaryMaxRaySample = raySample; float3 binaryRaySample = raySample; float depthThicknessFactor; - uint hitEyeIndex = sampleEyeIndex; for (int k = 0; k < binaryIterations; k++) { binaryRaySample = lerp(binaryMinRaySample, binaryMaxRaySample, 0.5); - Stereo::ResolveMonoUVForEye(binaryRaySample, eyeIndex, sampleUV, hitEyeIndex); - iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV, hitEyeIndex), 0).x; + sampleUV = binaryRaySample.xy; + iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV), 0).x; // Compute expected depth vs actual depth depthThicknessFactor = 1.0 - saturate(abs(binaryRaySample.z - iterationDepth) / SSRParams.y); @@ -91,17 +86,8 @@ float4 GetReflectionColor( float2 uvResultScreenCenterOffset = binaryRaySample.xy - 0.5; -# ifdef VR float2 centerDistance = abs(uvResultScreenCenterOffset.xy * 2.0); - // Make VR fades consistent by taking the closer of the two eyes - // Based on concepts from https://cuteloong.github.io/publications/scssr24/ - float2 otherEyeUvResultScreenCenterOffset = Stereo::ConvertMonoUVToOtherEye(float3(binaryRaySample.xy, iterationDepth), eyeIndex).xy - 0.5; - centerDistance = min(centerDistance, abs(otherEyeUvResultScreenCenterOffset * 2.0)); -# else - float2 centerDistance = abs(uvResultScreenCenterOffset.xy * 2.0); -# endif - // Fade out around screen edges float centerDistanceFadeFactorX = smoothstep(0.0, 0.1, saturate(1.0 - centerDistance.x)); float centerDistanceFadeFactorY = smoothstep(0.0, 0.5, saturate(1.0 - centerDistance.y)); @@ -109,21 +95,18 @@ float4 GetReflectionColor( float fadeFactor = depthThicknessFactor * ssrMarchingRadiusFadeFactor * centerDistanceFadeFactorX * centerDistanceFadeFactorY; if (fadeFactor > 0.0) { - // Resolve final UV in the eye that owns the hit - float2 finalSampleUV; - uint finalEyeIndex; - Stereo::ResolveMonoUVForEye(float3(binaryRaySample.xy, iterationDepth), eyeIndex, finalSampleUV, finalEyeIndex); + float2 finalSampleUV = binaryRaySample.xy; - float3 color = ColorTex.SampleLevel(ColorSampler, ConvertRaySample(finalSampleUV, finalEyeIndex), 0).xyz; + float3 color = ColorTex.SampleLevel(ColorSampler, ConvertRaySample(finalSampleUV), 0).xyz; // Final sample to world-space float4 positionWS = float4(float2(finalSampleUV.x, 1.0 - finalSampleUV.y) * 2.0 - 1.0, iterationDepth, 1.0); - positionWS = mul(FrameBuffer::CameraViewProjInverse[finalEyeIndex], positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; positionWS.w = 1.0; // Compute camera motion vector - float2 cameraMotionVector = MotionBlur::GetSSMotionVector(positionWS, positionWS, finalEyeIndex); + float2 cameraMotionVector = MotionBlur::GetSSMotionVector(positionWS, positionWS); // Reproject alpha from previous frame float2 reprojectedRaySample = finalSampleUV + cameraMotionVector; @@ -131,7 +114,7 @@ float4 GetReflectionColor( // Check that the reprojected data is within the frame if (!FrameBuffer::IsOutsideFrame(reprojectedRaySample.xy)) - alpha = float4(AlphaTex.SampleLevel(AlphaSampler, ConvertRaySamplePrevious(reprojectedRaySample.xy, finalEyeIndex), 0).xyz, 1.0); + alpha = float4(AlphaTex.SampleLevel(AlphaSampler, ConvertRaySamplePrevious(reprojectedRaySample.xy), 0).xyz, 1.0); float3 reflectionColor = color + SSRParams.z * alpha.xyz * alpha.w; return float4(reflectionColor, fadeFactor); @@ -154,12 +137,9 @@ PS_OUTPUT main(PS_INPUT input) return psout; # endif - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); float2 uv = input.TexCoord; float2 screenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); - uv = Stereo::ConvertFromStereoUV(uv, eyeIndex); - [branch] if (NormalTex.Sample(NormalSampler, screenPosition).z <= 0) { return psout; @@ -170,7 +150,7 @@ PS_OUTPUT main(PS_INPUT input) float depth = DepthTex.SampleLevel(DepthSampler, screenPosition, 0).x; float4 positionVS = float4(float2(uv.x, 1.0 - uv.y) * 2.0 - 1.0, depth, 1.0); - positionVS = mul(FrameBuffer::CameraProjInverse[eyeIndex], positionVS); + positionVS = mul(FrameBuffer::CameraProjInverse, positionVS); positionVS.xyz = positionVS.xyz / positionVS.w; float3 viewPosition = positionVS.xyz; @@ -184,14 +164,14 @@ PS_OUTPUT main(PS_INPUT input) } float4 reflectionPosition = float4(viewPosition + reflectionDirection, 1.0); - float4 projReflectionPosition = mul(FrameBuffer::CameraProj[eyeIndex], reflectionPosition); + float4 projReflectionPosition = mul(FrameBuffer::CameraProj, reflectionPosition); projReflectionPosition /= projReflectionPosition.w; projReflectionPosition.xy = projReflectionPosition.xy * float2(0.5, -0.5) + float2(0.5, 0.5); float3 projPosition = float3(uv, depth); float3 projReflectionDirection = normalize(projReflectionPosition.xyz - projPosition) * rayLength; - psout.Color = GetReflectionColor(projReflectionDirection, projPosition, eyeIndex); + psout.Color = GetReflectionColor(projReflectionDirection, projPosition); return psout; } diff --git a/package/Shaders/ISSAOComposite.hlsl b/package/Shaders/ISSAOComposite.hlsl index ecaf54ce1e..90e9edf6a8 100644 --- a/package/Shaders/ISSAOComposite.hlsl +++ b/package/Shaders/ISSAOComposite.hlsl @@ -157,13 +157,11 @@ PS_OUTPUT main(PS_INPUT input) } float snowMask = 0; -# if !defined(VR) if (EyePosition.w != 0) { float2 specSnow = snowSpecAlphaTex.Sample(snowSpecAlphaSampler, screenPosition).xy; composedColor.xyz += specSnow.x * specSnow.y; snowMask = specSnow.y; } -# endif # if defined(APPLY_SAO) if (EyePosition.w != 0 && 1e-5 < snowMask) { @@ -189,15 +187,14 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) bool exponentialHeightFogEnabled = SharedData::exponentialHeightFogSettings.enabled; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord.xy); - float2 monoUV = Stereo::ConvertFromStereoUV(input.TexCoord.xy, eyeIndex); + float2 monoUV = input.TexCoord.xy; float4 positionWS = float4(2 * float2(monoUV.x, -monoUV.y + 1) - 1, depth, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; float4 exponentialHeightFog = (float4)0; if (exponentialHeightFogEnabled) { - float4 fogScreenPosition = float4(Stereo::ConvertToStereoUV(monoUV, eyeIndex) * SharedData::BufferDim.xy, depth, 1.0f); - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, fogScreenPosition); + float4 fogScreenPosition = float4(monoUV * SharedData::BufferDim.xy, depth, 1.0f); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, fogScreenPosition); } if (isGeometryDepth || exponentialHeightFogEnabled) { float fogFade = exponentialHeightFogEnabled ? ExponentialHeightFog::GetVanillaFogFade(FogNearColor.w) : FogNearColor.w; @@ -220,16 +217,15 @@ PS_OUTPUT main(PS_INPUT input) # endif # endif -# if !defined(VR) float sparklesInput = 0; if (EyePosition.w != 0 && snowMask != 0 && 1e-5 < SparklesParameters2.z) { float shadowMask = shadowMaskTex.SampleLevel(shadowMaskSampler, screenPosition, 0).x; float4 vsPosition = float4(2 * input.TexCoord.x - 1, 1 - 2 * input.TexCoord.y, depth, 1); - float4 csPosition = mul(FrameBuffer::CameraViewProjInverse[0], vsPosition); + float4 csPosition = mul(FrameBuffer::CameraViewProjInverse, vsPosition); csPosition.xyz /= csPosition.w; - csPosition.xyz += FrameBuffer::CameraPosAdjust[0].xyz; + csPosition.xyz += FrameBuffer::CameraPosAdjust.xyz; float3 noiseSeed = 0.07 * (SparklesParameters2.x * csPosition.xyz); float noiseValue = 0.5 * (SimplexNoise(noiseSeed) + 1); @@ -248,7 +244,6 @@ PS_OUTPUT main(PS_INPUT input) composedColor *= 1 - SparklesParameters2.w; composedColor += sparklesColor; -# endif psout.Color = composedColor; diff --git a/package/Shaders/ISSAOMinify.hlsl b/package/Shaders/ISSAOMinify.hlsl index e82377f61c..3e8a31211c 100644 --- a/package/Shaders/ISSAOMinify.hlsl +++ b/package/Shaders/ISSAOMinify.hlsl @@ -44,7 +44,7 @@ PS_OUTPUT main(PS_INPUT input) float2 drAdjustedTexCoord = FrameBuffer::DynamicResolutionParams1.xy * input.TexCoord; float2 minifiedTexCoord = GetMinifiedTexCoord(drAdjustedTexCoord); finalTexCoord = clamp(minifiedTexCoord, 0, - FrameBuffer::DynamicResolutionParams1.xy - float2(FrameBuffer::CameraPreviousPosAdjust[0].w, 0)); + FrameBuffer::DynamicResolutionParams1.xy - float2(FrameBuffer::CameraPreviousPosAdjust.w, 0)); } else { finalTexCoord = GetMinifiedTexCoord(input.TexCoord); } diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index 332b9ebfe5..75950ba853 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -513,11 +513,7 @@ PS_OUTPUT main(PS_INPUT input) # else feedbackOut.x = saturate(sampleUV.x * motionReject.z); # endif -# if defined(VR) - colorOut.w = motionReject.x ? 1 : 0; -# else colorOut.w = 1; -# endif feedbackOut.w = 1; # ifdef HDR_OUTPUT diff --git a/package/Shaders/ISVolumetricLightingGenerateCS.hlsl b/package/Shaders/ISVolumetricLightingGenerateCS.hlsl index 7053aedfee..5cab1319fb 100644 --- a/package/Shaders/ISVolumetricLightingGenerateCS.hlsl +++ b/package/Shaders/ISVolumetricLightingGenerateCS.hlsl @@ -1,6 +1,5 @@ #include "Common/Math.hlsli" #include "Common/Random.hlsli" -#include "Common/VR.hlsli" #if defined(CSHADER) SamplerState ShadowmapSampler : register(s0); @@ -24,39 +23,21 @@ RWTexture3D DensityRW : register(u0); cbuffer PerTechnique : register(b0) { -# ifndef VR - row_major float4x4 CameraViewProj[1] : packoffset(c0); - row_major float4x4 CameraViewProjInverse[1] : packoffset(c4); - float4x3 ShadowMapProj[1][3] : packoffset(c8); + row_major float4x4 CameraViewProj : packoffset(c0); + row_major float4x4 CameraViewProjInverse : packoffset(c4); + float4x3 ShadowMapProj[3] : packoffset(c8); float3 EndSplitDistances : packoffset(c17.x); float ShadowMapCount : packoffset(c17.w); float EnableShadowCasting : packoffset(c18); float3 DirLightDirection : packoffset(c19); float3 TextureDimensions : packoffset(c20); - float3 WindInput[1] : packoffset(c21); + float3 WindInput : packoffset(c21); float InverseDensityScale : packoffset(c21.w); - float3 PosAdjust[1] : packoffset(c22); + float3 PosAdjust : packoffset(c22); float IterationIndex : packoffset(c22.w); float PhaseContribution : packoffset(c23.x); float PhaseScattering : packoffset(c23.y); float DensityContribution : packoffset(c23.z); -# else - row_major float4x4 CameraViewProj[2] : packoffset(c0); - row_major float4x4 CameraViewProjInverse[2] : packoffset(c8); - float4x3 ShadowMapProj[2][3] : packoffset(c16); - float3 EndSplitDistances : packoffset(c34.x); - float ShadowMapCount : packoffset(c34.w); - float EnableShadowCasting : packoffset(c35.x); - float3 DirLightDirection : packoffset(c36); - float3 TextureDimensions : packoffset(c37); - float3 WindInput[2] : packoffset(c38); - float InverseDensityScale : packoffset(c39.w); - float3 PosAdjust[2] : packoffset(c40); - float IterationIndex : packoffset(c41.w); - float PhaseContribution : packoffset(c42.x); - float PhaseScattering : packoffset(c42.y); - float DensityContribution : packoffset(c42.z); -# endif } [numthreads(32, 32, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { @@ -72,16 +53,14 @@ cbuffer PerTechnique : register(b0) }; float3 normalizedCoordinates = float3(dispatchID.xy + 0.5, dispatchID.z - 1.0) * rcp(TextureDimensions.xyz); - float2 uv = normalizedCoordinates.xy; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - float3 depthUv = Stereo::ConvertFromStereoUV(normalizedCoordinates, eyeIndex) + StepCoefficients[IterationIndex]; + float3 depthUv = normalizedCoordinates + StepCoefficients[IterationIndex]; float depth = InverseRepartitionTex.SampleLevel(InverseRepartitionSampler, depthUv.z, 0); float4 positionCS = float4(2 * depthUv.x - 1, 1 - 2 * depthUv.y, depth, 1); - float4 positionWS = mul(CameraViewProjInverse[eyeIndex], positionCS); + float4 positionWS = mul(CameraViewProjInverse, positionCS); positionWS *= rcp(positionWS.w); - float4 positionCSShifted = mul(CameraViewProj[eyeIndex], positionWS); + float4 positionCSShifted = mul(CameraViewProj, positionWS); positionCSShifted *= rcp(positionCSShifted.w); float shadowMapDepth = positionCSShifted.z; @@ -91,7 +70,7 @@ cbuffer PerTechnique : register(b0) uint cascadeIndex = ShadowMapCount >= 3.0f && shadowMapDepth > EndSplitDistances.y ? 2 : shadowMapDepth > EndSplitDistances.x ? 1 : 0; float shadowMapThreshold = cascadeIndex == 0 ? 0.01f : 0.0f; - float4x3 lightProjectionMatrix = ShadowMapProj[eyeIndex][cascadeIndex]; + float4x3 lightProjectionMatrix = ShadowMapProj[cascadeIndex]; float3 positionLS = mul(transpose(lightProjectionMatrix), float4(positionWS.xyz, 1)).xyz; float shadowMapValue = ShadowmapTex.SampleLevel(ShadowmapSampler, float3(positionLS.xy, cascadeIndex), 0); @@ -103,7 +82,7 @@ cbuffer PerTechnique : register(b0) } } - float3 noiseUv = 0.0125 * (InverseDensityScale * (positionWS.xyz + WindInput[eyeIndex])); + float3 noiseUv = 0.0125 * (InverseDensityScale * (positionWS.xyz + WindInput)); float noise = NoiseTex.SampleLevel(NoiseSampler, noiseUv, 0); float densityFactor = noise * (1 - 0.75 * smoothstep(0, 1, saturate(2 * positionWS.z / 300))); float densityContribution = lerp(1, densityFactor, DensityContribution); @@ -115,7 +94,7 @@ cbuffer PerTechnique : register(b0) float shadowContribution = noShadow; # if defined(TERRAIN_SHADOWS) || defined(CLOUD_SHADOWS) - shadowContribution *= sqrt(ShadowSampling::GetWorldShadow(positionWS.xyz, PosAdjust[eyeIndex], eyeIndex)); + shadowContribution *= sqrt(ShadowSampling::GetWorldShadow(positionWS.xyz, PosAdjust)); # endif float vl = shadowContribution * densityContribution * phaseContribution; diff --git a/package/Shaders/ISWaterBlend.hlsl b/package/Shaders/ISWaterBlend.hlsl index f3d9018d4a..6b8e256002 100644 --- a/package/Shaders/ISWaterBlend.hlsl +++ b/package/Shaders/ISWaterBlend.hlsl @@ -1,7 +1,6 @@ #include "Common/DummyVSTexCoord.hlsl" #include "Common/FrameBuffer.hlsli" #include "Common/Math.hlsli" -#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -43,7 +42,6 @@ namespace WaterBlend PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); float2 adjustedScreenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); float waterMask = waterMaskTex.Sample(waterMaskSampler, adjustedScreenPosition).z; if (waterMask < WaterBlend::WaterMaskThreshold) { @@ -52,17 +50,14 @@ PS_OUTPUT main(PS_INPUT input) float3 sourceColor = sourceTex.Sample(sourceSampler, adjustedScreenPosition).xyz; float2 motion = motionBufferTex.Sample(motionBufferSampler, adjustedScreenPosition).xy; - float2 motionScreenPosition = Stereo::ConvertToStereoUV(Stereo::ConvertFromStereoUV(input.TexCoord, eyeIndex) + motion, eyeIndex); + float2 motionScreenPosition = input.TexCoord + motion; float2 motionAdjustedScreenPosition = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(motionScreenPosition); float4 waterHistory = waterHistoryTex.Sample(waterHistorySampler, motionAdjustedScreenPosition).xyzw; float3 finalColor = sourceColor; - if ( -# ifndef VR - motionScreenPosition.x >= 0 && motionScreenPosition.y >= 0 && motionScreenPosition.x <= 1 && -# endif + if (motionScreenPosition.x >= 0 && motionScreenPosition.y >= 0 && motionScreenPosition.x <= 1 && motionScreenPosition.y <= 1 && waterHistory.w > 0.0) { float historyFactor = 0.95; if (NearFar_Menu_DistanceFactor.z == 0) { diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index e611a018cc..5a7740508f 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -11,7 +11,6 @@ #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" #include "Common/Triplanar.hlsli" -#include "Common/VR.hlsli" #if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) # define SKIN @@ -52,9 +51,6 @@ struct VS_INPUT #if defined(EYE) float EyeParameter: TEXCOORD2; #endif // EYE -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -90,28 +86,16 @@ struct VS_OUTPUT float4 Color: COLOR0; float4 FogParam: COLOR1; -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 -#endif - float3 ModelPosition: TEXCOORD12; }; #ifdef VSHADER cbuffer PerTechnique : register(b0) { -# if !defined(VR) - float4 HighDetailRange[1] : packoffset(c0); // loaded cells center in xy, size in zw + float4 HighDetailRange : packoffset(c0); // loaded cells center in xy, size in zw float4 FogParam : packoffset(c1); float4 FogNearColor : packoffset(c2); float4 FogFarColor : packoffset(c3); -# else - float4 HighDetailRange[2] : packoffset(c0); // loaded cells center in xy, size in zw - float4 FogParam : packoffset(c2); - float4 FogNearColor : packoffset(c3); - float4 FogFarColor : packoffset(c4); -# endif // VR }; cbuffer PerMaterial : register(b1) @@ -123,46 +107,25 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float3x4 World[1] : packoffset(c0); - row_major float3x4 PreviousWorld[1] : packoffset(c3); - float4 EyePosition[1] : packoffset(c6); + row_major float3x4 World : packoffset(c0); + row_major float3x4 PreviousWorld : packoffset(c3); + float4 EyePosition : packoffset(c6); float4 LandBlendParams : packoffset(c7); // offset in xy, gridPosition in yw float4 TreeParams : packoffset(c8); // wind magnitude in y, amplitude in z, leaf frequency in w float2 WindTimers : packoffset(c9); - row_major float3x4 TextureProj[1] : packoffset(c10); + row_major float3x4 TextureProj : packoffset(c10); float IndexScale : packoffset(c13); float4 WorldMapOverlayParameters : packoffset(c14); -# else // VR has 49 vs 30 entries - row_major float3x4 World[2] : packoffset(c0); - row_major float3x4 PreviousWorld[2] : packoffset(c6); - float4 EyePosition[2] : packoffset(c12); - float4 LandBlendParams : packoffset(c14); // offset in xy, gridPosition in yw - float4 TreeParams : packoffset(c15); // wind magnitude in y, amplitude in z, leaf frequency in w - float2 WindTimers : packoffset(c16); - row_major float3x4 TextureProj[2] : packoffset(c17); - float IndexScale : packoffset(c23); - float4 WorldMapOverlayParameters : packoffset(c24); -# endif // VR }; cbuffer VS_PerFrame : register(b12) { -# if !defined(VR) - row_major float3x3 ScreenProj[1] : packoffset(c0); - row_major float4x4 ViewProj[1] : packoffset(c8); -# if defined(SKINNED) - float3 BonesPivot[1] : packoffset(c40); - float3 PreviousBonesPivot[1] : packoffset(c41); -# endif // SKINNED -# else - row_major float3x3 ScreenProj[2] : packoffset(c0); - row_major float4x4 ViewProj[2] : packoffset(c16); -# if defined(SKINNED) - float3 BonesPivot[2] : packoffset(c80); - float3 PreviousBonesPivot[2] : packoffset(c82); -# endif // SKINNED -# endif // VR + row_major float3x3 ScreenProj : packoffset(c0); + row_major float4x4 ViewProj : packoffset(c8); +# if defined(SKINNED) + float3 BonesPivot : packoffset(c40); + float3 PreviousBonesPivot : packoffset(c41); +# endif // SKINNED }; # if defined(TREE_ANIM) @@ -182,13 +145,8 @@ VS_OUTPUT main(VS_INPUT input) precise float4 inputPosition = float4(input.Position.xyz, 1.0); - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif - ); # if defined(LODLANDNOISE) || defined(LODLANDSCAPE) - inputPosition = LodLandscape::AdjustLodLandscapeVertexPositionMS(inputPosition, float4x4(World[eyeIndex], float4(0, 0, 0, 1)), HighDetailRange[eyeIndex]); + inputPosition = LodLandscape::AdjustLodLandscapeVertexPositionMS(inputPosition, float4x4(World, float4(0, 0, 0, 1)), HighDetailRange); # endif // defined(LODLANDNOISE) || defined(LODLANDSCAPE) \ precise float4 previousInputPosition = inputPosition; @@ -205,19 +163,19 @@ VS_OUTPUT main(VS_INPUT input) precise int4 actualIndices = 765.01.xxxx * input.BoneIndices.xyzw; float3x4 previousWorldMatrix = - Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot[eyeIndex], input.BoneWeights); + Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot, input.BoneWeights); precise float4 previousWorldPosition = float4(mul(inputPosition, transpose(previousWorldMatrix)), 1); - float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot[eyeIndex], input.BoneWeights); + float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot, input.BoneWeights); precise float4 worldPosition = float4(mul(inputPosition, transpose(worldMatrix)), 1); - float4 viewPos = mul(ViewProj[eyeIndex], worldPosition); + float4 viewPos = mul(ViewProj, worldPosition); # else // !SKINNED - precise float4 previousWorldPosition = float4(mul(PreviousWorld[eyeIndex], inputPosition), 1); - precise float4 worldPosition = float4(mul(World[eyeIndex], inputPosition), 1); - precise float4x4 world4x4 = float4x4(World[eyeIndex][0], World[eyeIndex][1], World[eyeIndex][2], float4(0, 0, 0, 1)); - precise float4x4 modelView = mul(ViewProj[eyeIndex], world4x4); + precise float4 previousWorldPosition = float4(mul(PreviousWorld, inputPosition), 1); + precise float4 worldPosition = float4(mul(World, inputPosition), 1); + precise float4x4 world4x4 = float4x4(World[0], World[1], World[2], float4(0, 0, 0, 1)); + precise float4x4 modelView = mul(ViewProj, world4x4); float4 viewPos = mul(modelView, inputPosition); # endif // SKINNED @@ -231,8 +189,8 @@ VS_OUTPUT main(VS_INPUT input) # if defined(LANDSCAPE) vsout.TexCoord0.zw = (uv * 0.010416667.xx + LandBlendParams.xy) * float2(1, -1) + float2(0, 1); # elif defined(PROJECTED_UV) && !defined(SKINNED) - vsout.TexCoord0.z = mul(TextureProj[eyeIndex][0], inputPosition); - vsout.TexCoord0.w = mul(TextureProj[eyeIndex][1], inputPosition); + vsout.TexCoord0.z = mul(TextureProj[0], inputPosition); + vsout.TexCoord0.w = mul(TextureProj[1], inputPosition); # endif vsout.TexCoord0.xy = uv; @@ -262,9 +220,9 @@ VS_OUTPUT main(VS_INPUT input) vsout.TBN1.xyz = worldTbnTr[1]; vsout.TBN2.xyz = worldTbnTr[2]; # else - vsout.TBN0.xyz = mul(tbn, World[eyeIndex][0].xyz); - vsout.TBN1.xyz = mul(tbn, World[eyeIndex][1].xyz); - vsout.TBN2.xyz = mul(tbn, World[eyeIndex][2].xyz); + vsout.TBN0.xyz = mul(tbn, World[0].xyz); + vsout.TBN1.xyz = mul(tbn, World[1].xyz); + vsout.TBN2.xyz = mul(tbn, World[2].xyz); float3x3 tempTbnTr = transpose(float3x3(vsout.TBN0.xyz, vsout.TBN1.xyz, vsout.TBN2.xyz)); tempTbnTr[0] = normalize(tempTbnTr[0]); tempTbnTr[1] = normalize(tempTbnTr[1]); @@ -291,8 +249,8 @@ VS_OUTPUT main(VS_INPUT input) vsout.LandBlendWeights2.w = 1 - saturate(0.000375600968 * (9625.59961 - length(gridOffset))); vsout.LandBlendWeights2.xyz = input.LandBlendWeights2.xyz; # elif defined(PROJECTED_UV) && !defined(SKINNED) - float3x3 texProjWorld3x3 = float3x3(World[eyeIndex][0].xyz, World[eyeIndex][1].xyz, World[eyeIndex][2].xyz); - vsout.TexProj = mul(texProjWorld3x3, TextureProj[eyeIndex][2].xyz); + float3x3 texProjWorld3x3 = float3x3(World[0].xyz, World[1].xyz, World[2].xyz); + vsout.TexProj = mul(texProjWorld3x3, TextureProj[2].xyz); # endif # if defined(EYE) @@ -315,13 +273,6 @@ VS_OUTPUT main(VS_INPUT input) vsout.FogParam.xyz = lerp(FogNearColor.xyz, FogFarColor.xyz, fogColorParam); vsout.FogParam.w = fogColorParam; -# if defined(VR) - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); - vsout.Position = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR - vsout.ModelPosition = input.Position.xyz; return vsout; @@ -356,13 +307,6 @@ struct PS_OUTPUT #ifdef PSHADER -# if defined(VR_STEREO_OPT) && !defined(SNOW) -// POM depth offset UAV — written per-pixel for StereoBlendCS depth-aware reprojection. -// Bound at u7 (after the 7 deferred MRT slots 0-6) via OMSetRenderTargetsAndUnorderedAccessViews. -// -1.0 = no POM (sentinel, matches ClearPomOffsetTexture); >= 0 = POM ran (StereoBlendCS checks >= 0). -RWTexture2D PomOffsetTex : register(u7); -# endif - SamplerState SampTerrainParallaxSampler : register(s1); # if defined(LANDSCAPE) @@ -601,7 +545,6 @@ cbuffer PerMaterial : register(b1) # endif float4 CharacterLightParams : packoffset(c14); - // VR is [9] instead of [15] uint PBRFlags : packoffset(c15.x); float3 PBRParams1 : packoffset(c15.y); // roughness scale, displacement scale, specular level @@ -612,7 +555,6 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) float3 DirLightDirection : packoffset(c0); float3 DirLightColor : packoffset(c1); float4 ShadowLightMaskSelect : packoffset(c2); @@ -629,34 +571,12 @@ cbuffer PerGeometry : register(b2) float4 PointLightPosition[7] : packoffset(c15); // point light radius in w float4 PointLightColor[7] : packoffset(c22); float2 NumLightNumShadowLight : packoffset(c29); -# else - // VR is [49] instead of [30] - float3 DirLightDirection : packoffset(c0); - float4 UnknownPerGeometry[12] : packoffset(c1); - float3 DirLightColor : packoffset(c13); - float4 ShadowLightMaskSelect : packoffset(c14); - float4 MaterialData : packoffset(c15); // envmapLODFade in x, specularLODFade in y, alpha in z - float AlphaTestRef : packoffset(c16); - float3 EmitColor : packoffset(c16.y); - float4 ProjectedUVParams : packoffset(c18); - float4 SSRParams : packoffset(c19); - float4 WorldMapOverlayParametersPS : packoffset(c20); - float4 ProjectedUVParams2 : packoffset(c21); - float4 ProjectedUVParams3 : packoffset(c22); // fProjectedUVDiffuseNormalTilingScale in x, fProjectedUVNormalDetailTilingScale in y, EnableProjectedNormals in w - row_major float3x4 DirectionalAmbient : packoffset(c23); - float4 AmbientSpecularTintAndFresnelPower : packoffset(c26); // Fresnel power in z, color in xyz - float4 PointLightPosition[14] : packoffset(c27); // point light radius in w - float4 PointLightColor[7] : packoffset(c41); - float2 NumLightNumShadowLight : packoffset(c48); -# endif // VR }; -# if !defined(VR) cbuffer AlphaTestRefBuffer : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif float GetSoftLightMultiplier(float angle) { @@ -962,12 +882,11 @@ float GetSnowParameterY(float texProjTmp, float alpha) PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) { PS_OUTPUT psout; - uint eyeIndex = Stereo::GetEyeIndexPS(input.Position, VPOSOffset); - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; float3 viewDirection = -normalize(input.WorldPosition.xyz); - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); # if defined(DEFERRED) @@ -1038,9 +957,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // LANDSCAPE float sh0 = 0; float pixelOffset = 0; -# if defined(VR_STEREO_OPT) && !defined(SNOW) - bool hasPOM = false; -# endif # if defined(EMAT) # if defined(LANDSCAPE) @@ -1097,12 +1013,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(PARALLAX) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) if (SharedData::extendedMaterialSettings.EnableParallax) { mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset -# if defined(VR_STEREO_OPT) && !defined(SNOW) - , - hasPOM -# endif - ); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1135,12 +1046,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (envMaskSample.w > kMaskEpsilon && envMaskSample.w < (1.0 - kMaskEpsilon)) { complexMaterialParallax = true; mipLevel = ExtendedMaterials::GetMipLevel(uv, TexEnvMaskSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset -# if defined(VR_STEREO_OPT) && !defined(SNOW) - , - hasPOM -# endif - ); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, mipLevel).w; complexMaterialColor = TexEnvMaskSampler.Sample(SampEnvMaskSampler, uv); @@ -1187,12 +1093,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) displacementParams.HeightScale *= PBRParams1.y; } mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset -# if defined(VR_STEREO_OPT) && !defined(SNOW) - , - hasPOM -# endif - ); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1288,15 +1189,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) weights[0] = weights[1] = weights[2] = weights[3] = weights[4] = weights[5] = 0.0; # if defined(TERRAIN_VARIATION) uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, sharedOffset, dx, dy, pixelOffset, -# if defined(VR_STEREO_OPT) && !defined(SNOW) - hasPOM, -# endif weights); # else uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, pixelOffset, -# if defined(VR_STEREO_OPT) && !defined(SNOW) - hasPOM, -# endif weights); # endif if (SharedData::extendedMaterialSettings.EnableHeightBlending) { @@ -2084,9 +1979,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 worldNormal = normalize(mul(tbn, normal.xyz)); # if defined(TREE_ANIM) - float3 viewNormal = normalize(FrameBuffer::WorldToView(worldNormal, false, eyeIndex)); + float3 viewNormal = normalize(FrameBuffer::WorldToView(worldNormal, false)); viewNormal = float3(viewNormal.xy, -abs(viewNormal.z)); - worldNormal = normalize(FrameBuffer::ViewToWorld(viewNormal, false, eyeIndex)); + worldNormal = normalize(FrameBuffer::ViewToWorld(viewNormal, false)); # endif # if defined(SPARKLE) @@ -2102,7 +1997,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float2 wetUV = uv * SharedData::skinData.skinDetailParams.y; # endif - float2 dynamicWet = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); + float2 dynamicWet = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust.z, worldNormal.xyz); float skinWetness = Skin::PerlinNoise(wetUV, SharedData::skinData.wetParams.x, SharedData::skinData.wetParams.y, SharedData::skinData.wetParams.z, clamp(dynamicWet.x + dynamicWet.y + SharedData::skinData.skinParams2.y, 0.f, 2.f) * (hasSkinWetness ? 1.0 : 0.5)); if ((SharedData::skinData.skinDetailParams.w > 0.0f || skinWetness > 0.0f) && skinEnabled) # else @@ -2161,7 +2056,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float projWeight = 0; # if defined(PROJECTED_UV) - float3 projWorldPos = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 projWorldPos = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; float3 triFaceNormal = normalize(-cross(ddx(input.WorldPosition.xyz), ddy(input.WorldPosition.xyz))); float3 triWeights = Triplanar::GetWeights(tbnTr[2], triFaceNormal); float projNoise = Triplanar::Sample(TexCharacterLightProjNoiseSampler, SampCharacterLightProjNoiseSampler, projWorldPos, triWeights, ProjectedUVParams.z).x; @@ -2235,7 +2130,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 vertexNormal = worldNormal; # endif - float3 screenSpaceNormal = normalize(FrameBuffer::WorldToView(worldNormal, false, eyeIndex)); + float3 screenSpaceNormal = normalize(FrameBuffer::WorldToView(worldNormal, false)); # if defined(HAIR) && defined(CS_HAIR) float3 Bitangent = normalize(float3(input.TBN0.y, input.TBN1.y, input.TBN2.y)); @@ -2250,7 +2145,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::hairSpecularSettings.Enabled) { if (SharedData::hairSpecularSettings.EnableTangentShift && SharedData::hairSpecularSettings.HairMode != 1) { float3 shiftedNormal = Hair::ShiftWorldNormal(hairT, worldNormal, 0, uv); - screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false, eyeIndex)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false)); } } # endif @@ -2512,11 +2407,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float porosity = 1.0; # if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMSSkylight = input.WorldPosition.xyz; -# endif # if defined(DEFERRED) sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, worldNormal); # else @@ -2525,7 +2416,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif - float4 waterData = SharedData::GetWaterData(input.WorldPosition.xyz, eyeIndex); + float4 waterData = SharedData::GetWaterData(input.WorldPosition.xyz); float waterHeight = waterData.w; float waterRoughnessSpecular = 1; @@ -2560,9 +2451,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SKINNED) float3 ripplePosition = input.ModelPosition.xyz; # elif defined(DEFERRED) - float3 ripplePosition = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 ripplePosition = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; # else - float3 ripplePosition = !FrameBuffer::FrameParams.y ? input.ModelPosition.xyz : input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + float3 ripplePosition = !FrameBuffer::FrameParams.y ? input.ModelPosition.xyz : input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; # endif raindropInfo = WetnessEffects::GetRainDrops(ripplePosition, SharedData::wetnessEffectsSettings.Time, wetnessNormal, flatnessAmount); } @@ -2577,7 +2468,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(CS_SKIN) && !defined(SKIN) if (skinEnabled) { - float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); + float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust.z, worldNormal.xyz); # if defined(TRUE_PBR) dynamicWetness.x = lerp(dynamicWetness.x, 0.0f, material.Metallic); # endif @@ -2597,7 +2488,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(SKINNED) && !(defined(SKIN) && defined(CS_SKIN)) if (wetness > 0.0 || puddleWetness > 0.0) { - float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; + float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; puddle = Random::perlinNoise(puddleCoords) * 0.5 + 0.5; puddle = puddle * ((minWetnessAngle / SharedData::wetnessEffectsSettings.PuddleMaxAngle) * SharedData::wetnessEffectsSettings.MaxPuddleWetness * 0.25) + 0.5; puddle *= lerp(wetness, puddleWetness, saturate(puddle - 0.25)); @@ -2644,17 +2535,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif # if defined(WATER_EFFECTS) - dirLightColor *= WaterEffects::ComputeCaustics(waterData, input.WorldPosition.xyz, eyeIndex); + dirLightColor *= WaterEffects::ComputeCaustics(waterData, input.WorldPosition.xyz); # endif // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (inWorld || inReflection) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); float dirLightAngle = dot(worldNormal.xyz, DirLightDirection.xyz); @@ -2672,7 +2563,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(VOLUMETRIC_SHADOWS) if (inWorld && !inReflection && ShadowSampling::HasDirectionalShadows()) - dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, eyeIndex, dirVSMDetailedShadow); + dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, dirVSMDetailedShadow); # endif float dirDetailedShadow = 1.0; @@ -2689,7 +2580,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SCREEN_SPACE_SHADOWS) && defined(DEFERRED) if (!SharedData::InInterior && dirLightAngle >= 0.0) - dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise, eyeIndex); + dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise); # endif # if defined(EMAT) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) @@ -2754,7 +2645,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) dirLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, DirLightDirection, dirLightColor, dirDetailedShadow, dirSoftShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise, eyeIndex); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise); dirLightContext.hairShadow = hairShadow; } # endif @@ -2782,7 +2673,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(LIGHT_LIMIT_FIX) [loop] for (uint lightIndex = 0; lightIndex < numLights; lightIndex++) { - float3 lightDirection = PointLightPosition[eyeIndex * numLights + lightIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = PointLightPosition[lightIndex].xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); float intensityFactor = saturate(lightDist / PointLightPosition[lightIndex].w); if (intensityFactor == 1) @@ -2817,7 +2708,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, lightShadow, lightShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise); pointLightContext.hairShadow = hairShadow; } # endif @@ -2860,7 +2751,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) continue; } - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -2941,7 +2832,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, pointLightShadow, pointLightShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise); pointLightContext.hairShadow = hairShadow; } # endif @@ -3028,7 +2919,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) ambientNormal = normalize(viewDirection - hairT * dot(viewDirection, hairT)); else ambientNormal = vertexNormal.xyz; - screenSpaceNormal = normalize(FrameBuffer::WorldToView(ambientNormal, false, eyeIndex)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(ambientNormal, false)); } # endif @@ -3101,7 +2992,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lodLandDiffuseColor += directionalAmbientColor; # endif - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); # if defined(WETNESS_EFFECTS) # if !(defined(FACEGEN) || defined(FACEGEN_RGB_TINT) || defined(EYE)) || defined(TREE_ANIM) @@ -3157,7 +3048,6 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) color.xyz += directLightsDiffuseInput; } - // Fixes white items in UI for VR [branch] if ((PBRFlags & PBR::Flags::HasEmissive) != 0) { color.xyz += emitColor.xyz; @@ -3282,9 +3172,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::exponentialHeightFogSettings.enabled) { float4 exponentialHeightFog; if (inReflection) { - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); } else { - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); } fogColor = exponentialHeightFog.xyz; fogFactor = exponentialHeightFog.w; @@ -3463,7 +3353,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) indirectLobeWeights.specular += wetnessReflectance; if (waterRoughnessSpecular < 1) { // Reflection is from the water film surface; wetnessReflectance scales intensity by wetness amount. - screenSpaceNormal = normalize(FrameBuffer::WorldToView(wetnessNormal, false, eyeIndex)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(wetnessNormal, false)); material.Roughness = waterRoughnessSpecular; } # endif @@ -3471,11 +3361,14 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Reflectance = float4(indirectLobeWeights.specular, psout.Diffuse.w); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), saturate(1.0 - material.Roughness), psout.Diffuse.w); -# if defined(VR_STEREO_OPT) && !defined(SNOW) - // VR stereo reprojection: write POM depth offset to dedicated texture (u7) for StereoBlendCS. - // hasPOM disambiguates "POM ran at geometry plane (pixelOffset=0.5)" from "POM did not run". - // -1.0 is the explicit no-POM sentinel (R16_FLOAT supports negatives); StereoBlendCS checks >= 0. - PomOffsetTex[uint2(input.Position.xy)] = hasPOM ? pixelOffset : Stereo::POM_NO_DATA; +# if defined(SNOW) +# if defined(TRUE_PBR) + psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); + psout.Parameters.y = 0; +# else + psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); +# endif + psout.Parameters.w = psout.Diffuse.w; # endif float masksZ = Color::RGBToYCoCg(directionalAmbientColor).x; diff --git a/package/Shaders/Particle.hlsl b/package/Shaders/Particle.hlsl index c35f68861c..b21cc3c28a 100644 --- a/package/Shaders/Particle.hlsl +++ b/package/Shaders/Particle.hlsl @@ -1,7 +1,6 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" #include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" struct VS_INPUT { @@ -16,9 +15,6 @@ struct VS_INPUT int4 #endif TexCoord1: TEXCOORD1; -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -29,11 +25,6 @@ struct VS_OUTPUT #if defined(ENVCUBE) float4 PrecipitationOcclusionTexCoord: TEXCOORD1; #endif -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 - uint EyeIndex: EYEIDX0; -#endif // VR }; #ifdef VSHADER @@ -44,13 +35,8 @@ cbuffer PerTechnique : register(b0) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float4x4 WorldViewProj[1]; // 0 - row_major float4x4 WorldView[1]; // 4 -# else - row_major float4x4 WorldViewProj[2]; // 0 - row_major float4x4 WorldView[2]; // 8 -# endif + row_major float4x4 WorldViewProj; // 0 + row_major float4x4 WorldView; // 4 # if defined(ENVCUBE) row_major float4x4 PrecipitationOcclusionWorldViewProj; // 8, 16 # endif @@ -79,12 +65,6 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif - ); - # if defined(ENVCUBE) # if defined(RAIN) float2 positionOffset = input.TexCoord1.xy; @@ -101,11 +81,11 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz = normalizedPosition * fVars2.xxx + (-(fVars2.x * 0.5).xxx + fVars1.xyz); msPosition.w = 1; - float4 viewPosition = mul(WorldViewProj[eyeIndex], msPosition); + float4 viewPosition = mul(WorldViewProj, msPosition); # if defined(RAIN) float4 adjustedMsPosition = msPosition - float4(Velocity.xyz, 0); float positionBlendParam = 0.5 * (1 + input.TexCoord1.y); - float4 adjustedViewPosition = mul(WorldViewProj[eyeIndex], adjustedMsPosition); + float4 adjustedViewPosition = mul(WorldViewProj, adjustedMsPosition); float4 finalViewPosition = lerp(adjustedViewPosition, viewPosition, positionBlendParam); # else float4 finalViewPosition = viewPosition; @@ -160,7 +140,7 @@ VS_OUTPUT main(VS_INPUT input) input.Position.xyz)); msPosition.w = 1; - float4 viewPosition = mul(WorldViewProj[eyeIndex], msPosition); + float4 viewPosition = mul(WorldViewProj, msPosition); vsout.Position.xy = positionOffset * ScaleAdjust + viewPosition.xy; vsout.Position.zw = viewPosition.zw; @@ -194,13 +174,6 @@ VS_OUTPUT main(VS_INPUT input) vsout.Color.xyz = color.xyz; # endif -# ifdef VR - vsout.EyeIndex = eyeIndex; - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); - vsout.Position = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR return vsout; } #endif @@ -254,17 +227,8 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; -# if !defined(VR) - uint eyeIndex = 0; -# else - uint eyeIndex = input.EyeIndex; -# endif // !VR - # if defined(ENVCUBE) float2 precipitationOcclusionUV = (input.PrecipitationOcclusionTexCoord.xy * 0.5 + 0.5) * TextureSize.x; -# ifdef VR - precipitationOcclusionUV *= FrameBuffer::DynamicResolutionParams1.x; // only difference in VR -# endif float precipitationOcclusion = -input.PrecipitationOcclusionTexCoord.z + TexPrecipitationOcclusionTexture.Load(float3(precipitationOcclusionUV, 0)).x; float2 underwaterMaskUv = TextureSize.yz * input.Position.xy; float underwaterMask = TexUnderwaterMask.Sample(SampUnderwaterMask, underwaterMaskUv).x; @@ -289,14 +253,14 @@ PS_OUTPUT main(PS_INPUT input) float3 propertyColor = 0.0; - float2 uv = Stereo::ConvertFromStereoUV(input.Position.xy * SharedData::BufferDim.zw, eyeIndex); + float2 uv = input.Position.xy * SharedData::BufferDim.zw; float4 positionWS = float4(2 * float2(uv.x, -uv.y + 1) - 1, input.Position.z, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; float unusedDetailedShadow; - float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, eyeIndex, unusedDetailedShadow); + float3 dirLightColor = SharedData::DirLightColor.xyz * ShadowSampling::GetLightingShadow(positionWS.xyz, unusedDetailedShadow); float3 ambientColor = max(0, SharedData::GetAmbient(float3(0, 0, 1))); propertyColor += dirLightColor; @@ -305,8 +269,8 @@ PS_OUTPUT main(PS_INPUT input) # if defined(LIGHT_LIMIT_FIX) uint lightCount = 0; { - float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz, true, eyeIndex); - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz); + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); uint clusterIndex = 0; if (LightLimitFix::GetClusterIndex(screenUV, viewPosition.z, clusterIndex)) { @@ -319,7 +283,7 @@ PS_OUTPUT main(PS_INPUT input) if (LightLimitFix::IsLightIgnored(light) || light.lightFlags & LightLimitFix::LightFlags::Shadow) { continue; } - float3 lightDirection = light.positionWS[eyeIndex].xyz - positionWS.xyz; + float3 lightDirection = light.positionWS.xyz - positionWS.xyz; float lightDist = length(lightDirection); # if defined(ISL) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 8aaeecb4db..d41f5258de 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -27,9 +27,6 @@ struct VS_INPUT float4 InstanceData2: TEXCOORD5; float4 InstanceData3: TEXCOORD6; float4 InstanceData4: TEXCOORD7; -#ifdef VR - uint InstanceID: SV_INSTANCEID; -#endif // VR }; #ifdef GRASS_LIGHTING @@ -39,27 +36,13 @@ struct VS_OUTPUT float4 Color: COLOR0; float VertexMult: COLOR1; float3 TexCoord: TEXCOORD0; - float3 ViewSpacePosition: -# if !defined(VR) - TEXCOORD1; -# else - TEXCOORD2; -# endif + float3 ViewSpacePosition: TEXCOORD1; # if defined(RENDER_DEPTH) - float2 Depth: -# if !defined(VR) - TEXCOORD2; -# else - TEXCOORD3; -# endif + float2 Depth: TEXCOORD2; # endif // RENDER_DEPTH float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; float4 VertexNormal: POSITION4; -# ifdef VR - float ClipDistance: SV_ClipDistance0; - float CullDistance: SV_CullDistance0; -# endif // VR }; #else struct VS_OUTPUT @@ -75,14 +58,9 @@ struct VS_OUTPUT # endif // RENDER_DEPTH float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; -# ifdef VR - float ClipDistance: SV_ClipDistance0; - float CullDistance: SV_CullDistance0; -# endif // VR }; #endif -// Constant Buffers (Flat and VR) cbuffer PerGeometry : register( #ifdef VSHADER b2 @@ -91,11 +69,10 @@ cbuffer PerGeometry : register( #endif ) { -#if !defined(VR) - row_major float4x4 WorldViewProj[1] : packoffset(c0); - row_major float4x4 WorldView[1] : packoffset(c4); - row_major float4x4 World[1] : packoffset(c8); - row_major float4x4 PreviousWorld[1] : packoffset(c12); + row_major float4x4 WorldViewProj : packoffset(c0); + row_major float4x4 WorldView : packoffset(c4); + row_major float4x4 World : packoffset(c8); + row_major float4x4 PreviousWorld : packoffset(c12); float4 FogNearColor : packoffset(c16); float3 WindVector : packoffset(c17); float WindTimer : packoffset(c17.w); @@ -107,23 +84,6 @@ cbuffer PerGeometry : register( float AlphaParam2 : packoffset(c20.w); float3 ScaleMask : packoffset(c21); float ShadowClampValue : packoffset(c21.w); -#else - row_major float4x4 WorldViewProj[2] : packoffset(c0); - row_major float4x4 WorldView[2] : packoffset(c8); - row_major float4x4 World[2] : packoffset(c16); - row_major float4x4 PreviousWorld[2] : packoffset(c24); - float4 FogNearColor : packoffset(c32); - float3 WindVector : packoffset(c33); - float WindTimer : packoffset(c33.w); - float3 DirLightDirection : packoffset(c34); - float PreviousWindTimer : packoffset(c34.w); - float3 DirLightColor : packoffset(c35); - float AlphaParam1 : packoffset(c35.w); - float3 AmbientColor : packoffset(c36); - float AlphaParam2 : packoffset(c36.w); - float3 ScaleMask : packoffset(c37); - float ShadowClampValue : packoffset(c37.w); -#endif // !VR } #ifdef VSHADER @@ -190,11 +150,6 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif // VR - ); float3x3 world3x3 = float3x3(input.InstanceData2.xyz, input.InstanceData3.xyz, float3(input.InstanceData4.x, input.InstanceData2.w, input.InstanceData3.w)); float4 msPosition = GetMSPosition(input, world3x3); @@ -210,10 +165,8 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz += windDisplacement; - float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); -# if !defined(VR) + float4 projSpacePosition = mul(WorldViewProj, msPosition); vsout.HPosition = projSpacePosition; -# endif // !VR # if defined(RENDER_DEPTH) vsout.Depth = projSpacePosition.zw; @@ -221,11 +174,7 @@ VS_OUTPUT main(VS_INPUT input) float perInstanceFade = dot(cb8[(asuint(cb7[0].x) >> 2)].xyzw, Math::IdentityMatrix[(asint(cb7[0].x) & 3)].xyzw); -# if defined(VR) - float distanceFade = 1 - saturate((length(mul(World[0], msPosition).xyz) - AlphaParam1) / AlphaParam2); -# else float distanceFade = 1 - saturate((length(projSpacePosition.xyz) - AlphaParam1) / AlphaParam2); -# endif // Note: input.Color.w is used for wind speed vsout.Color.xyz = input.Color.xyz; @@ -235,8 +184,8 @@ VS_OUTPUT main(VS_INPUT input) vsout.TexCoord.xy = input.TexCoord.xy; vsout.TexCoord.z = FogNearColor.w; - vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; - vsout.WorldPosition = mul(World[eyeIndex], msPosition); + vsout.ViewSpacePosition = mul(WorldView, msPosition).xyz; + vsout.WorldPosition = mul(World, msPosition); float4 previousMsPosition = GetMSPosition(input, world3x3); @@ -246,13 +195,7 @@ VS_OUTPUT main(VS_INPUT input) previousMsPosition.xyz += previousWindDisplacement; - vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); -# if defined(VR) - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); - vsout.HPosition = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // !VR + vsout.PreviousWorldPosition = mul(PreviousWorld, previousMsPosition); // Vertex normal needs to be transformed to world-space for lighting calculations. vsout.VertexNormal.xyz = mul(world3x3, input.Normal.xyz * 2.0 - 1.0); @@ -265,12 +208,6 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif // VR - ); - float4 msPosition = GetMSPosition(input); float3 windDisplacement = CalculateWindDisplacement(input, WindTimer); @@ -284,10 +221,8 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz += windDisplacement; - float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); -# if !defined(VR) + float4 projSpacePosition = mul(WorldViewProj, msPosition); vsout.HPosition = projSpacePosition; -# endif // !VR vsout.HPosition = projSpacePosition; # if defined(RENDER_DEPTH) vsout.Depth = projSpacePosition.zw; @@ -299,11 +234,7 @@ VS_OUTPUT main(VS_INPUT input) float perInstanceFade = dot(cb8[(asuint(cb7[0].x) >> 2)].xyzw, Math::IdentityMatrix[(asint(cb7[0].x) & 3)].xyzw); -# if defined(VR) - float distanceFade = 1 - saturate((length(mul(World[0], msPosition).xyz) - AlphaParam1) / AlphaParam2); -# else float distanceFade = 1 - saturate((length(projSpacePosition.xyz) - AlphaParam1) / AlphaParam2); -# endif vsout.Color.xyz = input.Color.xyz; vsout.Color.w = distanceFade * perInstanceFade; @@ -315,16 +246,10 @@ VS_OUTPUT main(VS_INPUT input) vsout.AmbientColor.xyz = input.InstanceData1.www * (AmbientColor.xyz * input.Color.xyz); vsout.AmbientColor.w = ShadowClampValue; - vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; - vsout.WorldPosition = mul(World[eyeIndex], msPosition); + vsout.ViewSpacePosition = mul(WorldView, msPosition).xyz; + vsout.WorldPosition = mul(World, msPosition); float4 previousMsPosition = GetMSPosition(input); -# if defined(VR) - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); - vsout.HPosition = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // !VR # ifdef GRASS_COLLISION previousMsPosition.xyz += previousDisplacement; @@ -332,7 +257,7 @@ VS_OUTPUT main(VS_INPUT input) previousMsPosition.xyz += previousWindDisplacement; - vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld, previousMsPosition); return vsout; } @@ -408,12 +333,10 @@ Texture2D TexSubsurfaceSampler : register(t4); # endif // GRASS_LIGHTING -# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif // !VR # if defined(SCREEN_SPACE_SHADOWS) # include "ScreenSpaceShadows/ScreenSpaceShadows.hlsli" @@ -521,14 +444,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float4 specColor = TexNormalSampler.SampleBias(SampNormalSampler, input.TexCoord.xy, SharedData::MipBias); # endif - uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); - psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); float3 viewDirection = -normalize(input.WorldPosition.xyz); float3 normal = normalize(input.VertexNormal.xyz); - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); // Swaps direction of the backfaces otherwise they seem to get lit from the wrong direction. @@ -587,7 +509,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif @@ -597,7 +519,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (!SharedData::InInterior) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); float dirDetailedShadow = 1.0; @@ -606,7 +528,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SCREEN_SPACE_SHADOWS) if (!SharedData::InInterior && dirLightAngle >= 0.0) - dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); + dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise); # endif // SCREEN_SPACE_SHADOWS float3 diffuseColor = 0; @@ -636,11 +558,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMSSkylight = input.WorldPosition.xyz; -# endif float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -666,7 +584,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + i]; LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -781,7 +699,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Diffuse.xyz = diffuseColor; # endif - float3 normalVS = normalize(FrameBuffer::WorldToView(normal, false, eyeIndex)); + float3 normalVS = normalize(FrameBuffer::WorldToView(normal, false)); # if defined(TRUE_PBR) psout.Albedo = float4(Color::IrradianceToGamma(indirectDiffuseLobeWeight), 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), 1 - pbrSurfaceProperties.Roughness, 1); @@ -819,10 +737,8 @@ PS_OUTPUT main(PS_INPUT input) if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) input.Color.xyz = 1; - uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); - - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); float4 shadowColor = TexShadowMaskSampler.Load(int3(input.HPosition.xy, 0)); @@ -832,7 +748,7 @@ PS_OUTPUT main(PS_INPUT input) // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (!SharedData::InInterior) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); float dirDetailedShadow = 1.0; @@ -841,7 +757,7 @@ PS_OUTPUT main(PS_INPUT input) # if defined(SCREEN_SPACE_SHADOWS) if (!SharedData::InInterior) - dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise, eyeIndex); + dirDetailedShadow *= ScreenSpaceShadows::GetScreenSpaceShadow(input.HPosition.xyz, screenUV, screenNoise); # endif // SCREEN_SPACE_SHADOWS float3 diffuseColor = dirLightColor * dirDetailedShadow; @@ -860,7 +776,7 @@ PS_OUTPUT main(PS_INPUT input) uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + i]; LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -903,11 +819,7 @@ PS_OUTPUT main(PS_INPUT input) vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) -# if defined(VR) - float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMSSkylight = input.WorldPosition.xyz; -# endif float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -943,8 +855,8 @@ PS_OUTPUT main(PS_INPUT input) psout.Diffuse.w = 1; - psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); - psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false, eyeIndex)); + psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false)); psout.Normal.zw = 0; psout.Albedo = float4(albedo, 1); diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 1d4be8643c..4d483ef0c5 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -3,7 +3,6 @@ #include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" struct VS_INPUT { @@ -14,9 +13,6 @@ struct VS_INPUT #endif float4 Color: COLOR0; -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -46,43 +42,23 @@ struct VS_OUTPUT float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; float3 FogPosition: TEXCOORD4; -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 - uint EyeIndex: EYEIDX0; -#endif // VR }; #ifdef VSHADER cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float4x4 WorldViewProj[1] : packoffset(c0); - row_major float4x4 World[1] : packoffset(c4); - row_major float4x4 PreviousWorld[1] : packoffset(c8); - float3 EyePosition[1] : packoffset(c12); + row_major float4x4 WorldViewProj : packoffset(c0); + row_major float4x4 World : packoffset(c4); + row_major float4x4 PreviousWorld : packoffset(c8); + float3 EyePosition : packoffset(c12); float VParams : packoffset(c12.w); float4 BlendColor[3] : packoffset(c13); float2 TexCoordOff : packoffset(c16); -# else - row_major float4x4 WorldViewProj[2] : packoffset(c0); - row_major float4x4 World[2] : packoffset(c8); - row_major float4x4 PreviousWorld[2] : packoffset(c16); - float3 EyePosition[2] : packoffset(c24); - float VParams : packoffset(c25.w); - float4 BlendColor[3] : packoffset(c26); - float2 TexCoordOff : packoffset(c29); -# endif // !VR }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif - ); float4 inputPosition = float4(input.Position.xyz, 1.0); @@ -97,8 +73,8 @@ VS_OUTPUT main(VS_INPUT input) # elif defined(HORIZFADE) - float worldHeight = mul(World[eyeIndex], inputPosition).z; - float eyeHeightDelta = -EyePosition[eyeIndex].z + worldHeight; + float worldHeight = mul(World, inputPosition).z; + float eyeHeightDelta = -EyePosition.z + worldHeight; vsout.TexCoord0.xy = input.TexCoord; vsout.TexCoord2.x = saturate((1.0 / 17.0) * eyeHeightDelta); @@ -137,18 +113,11 @@ VS_OUTPUT main(VS_INPUT input) # endif // OCCLUSION MOONMASK HORIZFADE - vsout.Position = mul(WorldViewProj[eyeIndex], inputPosition).xyww; - vsout.WorldPosition = mul(World[eyeIndex], inputPosition); - vsout.FogPosition = vsout.WorldPosition.xyz - EyePosition[eyeIndex].xyz; - vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], inputPosition); - -# ifdef VR - vsout.EyeIndex = eyeIndex; - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); - vsout.Position = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR + vsout.Position = mul(WorldViewProj, inputPosition).xyww; + vsout.WorldPosition = mul(World, inputPosition); + vsout.FogPosition = vsout.WorldPosition.xyz - EyePosition.xyz; + vsout.PreviousWorldPosition = mul(PreviousWorld, inputPosition); + return vsout; } #endif @@ -179,12 +148,10 @@ cbuffer PerGeometry : register(b2) float2 PParams : packoffset(c0); }; -# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif # include "Common/MotionBlur.hlsli" # include "Common/SharedData.hlsli" @@ -211,11 +178,6 @@ PS_OUTPUT main(PS_INPUT input) // Color::Sky is float3->float3 (per-channel sky gamma). PParams.yyy broadcasts the packed // scalar in PParams.y to RGB; float3 matches output .xyz where skyScale is added. float3 skyScale = Color::Sky(PParams.yyy); -# if !defined(VR) - uint eyeIndex = 0; -# else - uint eyeIndex = input.EyeIndex; -# endif // !VR # ifndef OCCLUSION # ifndef TEXLERP @@ -293,12 +255,12 @@ PS_OUTPUT main(PS_INPUT input) const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { float3 skyFogPosition = normalize(input.FogPosition.xyz) * SharedData::CameraData.x; - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(skyFogPosition, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, psout.Color.xyz, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(skyFogPosition, FrameBuffer::CameraPosAdjust.xyz, psout.Color.xyz, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); psout.Color.xyz = lerp(psout.Color.xyz, exponentialHeightFog.xyz, exponentialHeightFog.w); } # endif - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); psout.MotionVectors = float4(screenMotionVector, 0, psout.Color.w); psout.Normal = float4(0.5, 0.5, 0, psout.Color.w); diff --git a/package/Shaders/Tests/TestVR.hlsl b/package/Shaders/Tests/TestVR.hlsl deleted file mode 100644 index 1d528386c9..0000000000 --- a/package/Shaders/Tests/TestVR.hlsl +++ /dev/null @@ -1,362 +0,0 @@ -// HLSL Unit Tests for Common/VR.hlsli -// Tests the pure-math UV conversion functions that form the foundation of VR stereo rendering. -// These run with VR defined so the stereo code paths are exercised. -// COMPUTESHADER prevents FrameBuffer.hlsli inclusion (we only need the UV math). -#define VR -#define COMPUTESHADER -#include "/Shaders/Common/VR.hlsli" -#include "/Test/STF/ShaderTestFramework.hlsli" - -static const float kEps = 0.0001f; - -/// @tags vr, stereo, uv -/// ConvertToStereoUV: left eye maps [0,1] -> [0,0.5] -[numthreads(1, 1, 1)] void TestConvertToStereoUVLeftEye() { - float2 uv = float2(0.0, 0.5); - float2 result = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(result.x - 0.0) < kEps); - ASSERT(IsTrue, abs(result.y - 0.5) < kEps); - - uv = float2(1.0, 0.5); - result = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); - ASSERT(IsTrue, abs(result.y - 0.5) < kEps); - - uv = float2(0.5, 0.25); - result = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(result.x - 0.25) < kEps); - ASSERT(IsTrue, abs(result.y - 0.25) < kEps); -} - - /// @tags vr, stereo, uv - /// ConvertToStereoUV: right eye maps [0,1] -> [0.5,1] - [numthreads(1, 1, 1)] void TestConvertToStereoUVRightEye() -{ - float2 uv = float2(0.0, 0.5); - float2 result = Stereo::ConvertToStereoUV(uv, 1); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); - ASSERT(IsTrue, abs(result.y - 0.5) < kEps); - - uv = float2(1.0, 0.5); - result = Stereo::ConvertToStereoUV(uv, 1); - ASSERT(IsTrue, abs(result.x - 1.0) < kEps); - - uv = float2(0.5, 0.25); - result = Stereo::ConvertToStereoUV(uv, 1); - ASSERT(IsTrue, abs(result.x - 0.75) < kEps); -} - -/// @tags vr, stereo, uv -/// ConvertToStereoUV with Y inversion -[numthreads(1, 1, 1)] void TestConvertToStereoUVInvertY() { - float2 uv = float2(0.5, 0.25); - float2 result = Stereo::ConvertToStereoUV(uv, 0, 1); - ASSERT(IsTrue, abs(result.x - 0.25) < kEps); - ASSERT(IsTrue, abs(result.y - 0.75) < kEps); -} - - /// @tags vr, stereo, uv - /// ConvertFromStereoUV: left eye maps [0,0.5] -> [0,1] - [numthreads(1, 1, 1)] void TestConvertFromStereoUVLeftEye() -{ - float2 stereoUV = float2(0.0, 0.5); - float2 result = Stereo::ConvertFromStereoUV(stereoUV, 0); - ASSERT(IsTrue, abs(result.x - 0.0) < kEps); - ASSERT(IsTrue, abs(result.y - 0.5) < kEps); - - stereoUV = float2(0.5, 0.5); - result = Stereo::ConvertFromStereoUV(stereoUV, 0); - ASSERT(IsTrue, abs(result.x - 1.0) < kEps); - - stereoUV = float2(0.25, 0.25); - result = Stereo::ConvertFromStereoUV(stereoUV, 0); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); -} - -/// @tags vr, stereo, uv -/// ConvertFromStereoUV: right eye maps [0.5,1] -> [0,1] -[numthreads(1, 1, 1)] void TestConvertFromStereoUVRightEye() { - float2 stereoUV = float2(0.5, 0.5); - float2 result = Stereo::ConvertFromStereoUV(stereoUV, 1); - ASSERT(IsTrue, abs(result.x - 0.0) < kEps); - - stereoUV = float2(1.0, 0.5); - result = Stereo::ConvertFromStereoUV(stereoUV, 1); - ASSERT(IsTrue, abs(result.x - 1.0) < kEps); - - stereoUV = float2(0.75, 0.25); - result = Stereo::ConvertFromStereoUV(stereoUV, 1); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); -} - - /// @tags vr, stereo, uv - /// ConvertToStereoUV and ConvertFromStereoUV are inverses of each other - [numthreads(1, 1, 1)] void TestStereoUVRoundTrip() -{ - float2 original = float2(0.3, 0.7); - - // Left eye round-trip - float2 stereo = Stereo::ConvertToStereoUV(original, 0); - float2 recovered = Stereo::ConvertFromStereoUV(stereo, 0); - ASSERT(IsTrue, abs(recovered.x - original.x) < kEps); - ASSERT(IsTrue, abs(recovered.y - original.y) < kEps); - - // Right eye round-trip - stereo = Stereo::ConvertToStereoUV(original, 1); - recovered = Stereo::ConvertFromStereoUV(stereo, 1); - ASSERT(IsTrue, abs(recovered.x - original.x) < kEps); - ASSERT(IsTrue, abs(recovered.y - original.y) < kEps); -} - -/// @tags vr, stereo, uv -/// GetEyeIndexFromTexCoord: left half -> 0, right half -> 1 -[numthreads(1, 1, 1)] void TestGetEyeIndexFromTexCoord() { - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.0, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.25, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.49, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.5, 0.5)), 1u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.75, 0.5)), 1u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(1.0, 0.5)), 1u); -} - - /// @tags vr, stereo, uv - /// GetEyeIndexFromTexCoord is consistent with ConvertToStereoUV output - [numthreads(1, 1, 1)] void TestEyeIndexConsistentWithStereoUV() -{ - float2 monoUV = float2(0.6, 0.4); - - // Convert to stereo for left eye, then detect eye index - float2 stereoLeft = Stereo::ConvertToStereoUV(monoUV, 0); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(stereoLeft), 0u); - - // Convert to stereo for right eye, then detect eye index - float2 stereoRight = Stereo::ConvertToStereoUV(monoUV, 1); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(stereoRight), 1u); -} - -/// @tags vr, stereo, depth, edge-detection -/// MaxDepthDiff: identical neighbors -> 0 -[numthreads(1, 1, 1)] void TestMaxDepthDiffAllSame() { - float result = Stereo::MaxDepthDiff(0.5, float4(0.5, 0.5, 0.5, 0.5)); - ASSERT(IsTrue, abs(result) < kEps); -} - - /// @tags vr, stereo, depth, edge-detection - /// MaxDepthDiff: returns |center - neighbor| when one neighbor differs - [numthreads(1, 1, 1)] void TestMaxDepthDiffOneDiffers() -{ - // Only .z differs - float result = Stereo::MaxDepthDiff(0.5, float4(0.5, 0.5, 0.8, 0.5)); - ASSERT(IsTrue, abs(result - 0.3) < kEps); -} - -/// @tags vr, stereo, depth, edge-detection -/// MaxDepthDiff: returns the largest difference across all four neighbors -[numthreads(1, 1, 1)] void TestMaxDepthDiffPicksLargest() { - float result = Stereo::MaxDepthDiff(0.5, float4(0.55, 0.45, 0.9, 0.48)); - ASSERT(IsTrue, abs(result - 0.4) < kEps); // abs(0.5 - 0.9) = 0.4 -} - - /// @tags vr, stereo, depth, edge-detection - /// MaxDepthDiff: arm/world case returns exact diff (arm=0.75, world=1.0 -> 0.25) - [numthreads(1, 1, 1)] void TestMaxDepthDiffArmWorldCase() -{ - float armDepth = 0.75; - float worldDepth = 1.0; - float result = Stereo::MaxDepthDiff(armDepth, float4(worldDepth, armDepth, armDepth, armDepth)); - ASSERT(IsTrue, abs(result - abs(worldDepth - armDepth)) < kEps); -} - -/// @tags vr, stereo, depth, edge-detection -/// MaxDepthDiff: symmetric - diff(a,b) == diff(b,a) -[numthreads(1, 1, 1)] void TestMaxDepthDiffSymmetry() { - float a = 0.3, b = 0.7; - float fwd = Stereo::MaxDepthDiff(a, float4(b, a, a, a)); - float rev = Stereo::MaxDepthDiff(b, float4(a, b, b, b)); - ASSERT(IsTrue, abs(fwd - rev) < kEps); -} - - /// @tags vr, stereo, depth, edge-detection - /// MaxDepthDiff: center == 0 (mask pixel) against world neighbor - [numthreads(1, 1, 1)] void TestMaxDepthDiffMaskCenter() -{ - float result = Stereo::MaxDepthDiff(0.0, float4(0.8, 0.0, 0.0, 0.0)); - ASSERT(IsTrue, abs(result - 0.8) < kEps); -} - -/// @tags vr, stereo, edge-detection -/// ClampToEyeBounds: interior pixel is returned unchanged for both eyes -[numthreads(1, 1, 1)] void TestClampToEyeBoundsInterior() { - float2 frameDim = float2(2048, 1024); - int2 left = Stereo::ClampToEyeBounds(int2(512, 512), 0, frameDim); - ASSERT(AreEqual, left.x, 512); - ASSERT(AreEqual, left.y, 512); - - int2 right = Stereo::ClampToEyeBounds(int2(1536, 512), 1, frameDim); - ASSERT(AreEqual, right.x, 1536); - ASSERT(AreEqual, right.y, 512); -} - - /// @tags vr, stereo, edge-detection - /// ClampToEyeBounds: left eye x cannot cross the half-width seam - [numthreads(1, 1, 1)] void TestClampToEyeBoundsLeftEyeSeam() -{ - float2 frameDim = float2(2048, 1024); - // x past the seam clamps to halfWidth - 1 = 1023 - int2 result = Stereo::ClampToEyeBounds(int2(1025, 512), 0, frameDim); - ASSERT(AreEqual, result.x, 1023); -} - -/// @tags vr, stereo, edge-detection -/// ClampToEyeBounds: right eye x cannot cross the half-width seam -[numthreads(1, 1, 1)] void TestClampToEyeBoundsRightEyeSeam() { - float2 frameDim = float2(2048, 1024); - // x before the seam clamps to halfWidth = 1024 - int2 result = Stereo::ClampToEyeBounds(int2(1022, 512), 1, frameDim); - ASSERT(AreEqual, result.x, 1024); -} - - /// @tags vr, stereo, edge-detection - /// ClampToEyeBounds: x clamped at outer borders (left eye left edge, right eye right edge) - [numthreads(1, 1, 1)] void TestClampToEyeBoundsOuterBorders() -{ - float2 frameDim = float2(2048, 1024); - int2 leftBorder = Stereo::ClampToEyeBounds(int2(-1, 512), 0, frameDim); - ASSERT(AreEqual, leftBorder.x, 0); - - int2 rightBorder = Stereo::ClampToEyeBounds(int2(2049, 512), 1, frameDim); - ASSERT(AreEqual, rightBorder.x, 2047); -} - -/// @tags vr, stereo, edge-detection -/// ClampToEyeBounds: y is clamped to [0, frameDim.y - 1] independently of eye -[numthreads(1, 1, 1)] void TestClampToEyeBoundsY() { - float2 frameDim = float2(2048, 1024); - int2 top = Stereo::ClampToEyeBounds(int2(512, -1), 0, frameDim); - ASSERT(AreEqual, top.y, 0); - - int2 bottom = Stereo::ClampToEyeBounds(int2(512, 1025), 0, frameDim); - ASSERT(AreEqual, bottom.y, 1023); -} - - /// @tags vr, stereo, edge-detection - /// ClampToEyeUV: interior UV is returned unchanged for both eyes - [numthreads(1, 1, 1)] void TestClampToEyeUVInterior() -{ - float2 left = Stereo::ClampToEyeUV(float2(0.25, 0.5), 0); - ASSERT(IsTrue, abs(left.x - 0.25) < kEps); - ASSERT(IsTrue, abs(left.y - 0.5) < kEps); - - float2 right = Stereo::ClampToEyeUV(float2(0.75, 0.5), 1); - ASSERT(IsTrue, abs(right.x - 0.75) < kEps); - ASSERT(IsTrue, abs(right.y - 0.5) < kEps); -} - -/// @tags vr, stereo, edge-detection -/// ClampToEyeUV: left eye x cannot cross the x=0.5 seam -[numthreads(1, 1, 1)] void TestClampToEyeUVLeftEyeSeam() { - // x past the seam clamps to 0.5 - float2 result = Stereo::ClampToEyeUV(float2(0.6, 0.5), 0); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); -} - - /// @tags vr, stereo, edge-detection - /// ClampToEyeUV: right eye x cannot cross the x=0.5 seam - [numthreads(1, 1, 1)] void TestClampToEyeUVRightEyeSeam() -{ - // x before the seam clamps to 0.5 - float2 result = Stereo::ClampToEyeUV(float2(0.4, 0.5), 1); - ASSERT(IsTrue, abs(result.x - 0.5) < kEps); -} - -/// @tags vr, stereo, edge-detection -/// ClampToEyeUV: x clamped at outer borders (left eye at 0.0, right eye at 1.0) -[numthreads(1, 1, 1)] void TestClampToEyeUVOuterBorders() { - float2 leftBorder = Stereo::ClampToEyeUV(float2(-0.1, 0.5), 0); - ASSERT(IsTrue, abs(leftBorder.x - 0.0) < kEps); - - float2 rightBorder = Stereo::ClampToEyeUV(float2(1.1, 0.5), 1); - ASSERT(IsTrue, abs(rightBorder.x - 1.0) < kEps); -} - - /// @tags vr, stereo, edge-detection - /// ClampToEyeUV: y coordinate is not modified - [numthreads(1, 1, 1)] void TestClampToEyeUVYUnchanged() -{ - float2 result = Stereo::ClampToEyeUV(float2(0.25, 1.5), 0); - ASSERT(IsTrue, abs(result.y - 1.5) < kEps); - - result = Stereo::ClampToEyeUV(float2(0.75, -0.5), 1); - ASSERT(IsTrue, abs(result.y - (-0.5)) < kEps); -} - -/// @tags vr, stereo, uv -/// ConvertToStereoUV clamps input x to [0,1] via saturate -[numthreads(1, 1, 1)] void TestConvertToStereoUVClamping() { - // x > 1 should be clamped to 1 before conversion - float2 uv = float2(1.5, 0.5); - float2 resultLeft = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(resultLeft.x - 0.5) < kEps); // saturate(1.5)=1.0, (1.0+0)/2=0.5 - - float2 resultRight = Stereo::ConvertToStereoUV(uv, 1); - ASSERT(IsTrue, abs(resultRight.x - 1.0) < kEps); // saturate(1.5)=1.0, (1.0+1)/2=1.0 - - // x < 0 should be clamped to 0 - uv = float2(-0.5, 0.5); - resultLeft = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(resultLeft.x - 0.0) < kEps); // saturate(-0.5)=0.0, (0+0)/2=0 -} - - /// @tags vr, stereo, uv - /// ConvertUVToNormalizedScreenSpace maps to [-1,1] range - [numthreads(1, 1, 1)] void TestConvertUVToNormalizedScreenSpace() -{ - // Center of left eye (stereo UV 0.25) -> x should be near 0 (center of that eye) - float2 result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 0.5)); - ASSERT(IsTrue, abs(result.x - 0.0) < kEps); - ASSERT(IsTrue, abs(result.y - 0.0) < kEps); - - // Center of right eye (stereo UV 0.75) -> x should also be near 0 - result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.75, 0.5)); - ASSERT(IsTrue, abs(result.x - 0.0) < kEps); - - // Outer edges (stereo UV 0.0 and 1.0) -> x = +1 - result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.0, 0.5)); - ASSERT(IsTrue, abs(result.x - 1.0) < kEps); - - // Inner edge / midpoint (stereo UV 0.5) -> x = -1 - result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.5, 0.5)); - ASSERT(IsTrue, abs(result.x - (-1.0)) < kEps); - - // Top -> y = -1, bottom -> y = 1 - result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 0.0)); - ASSERT(IsTrue, abs(result.y - (-1.0)) < kEps); - - result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 1.0)); - ASSERT(IsTrue, abs(result.y - 1.0) < kEps); -} - -/// @tags vr, stereo, uv -/// ApplyVelocityToUV: correctly translates UV keeping stereoscopic boundaries intact and reports bounds -[numthreads(1, 1, 1)] void TestApplyVelocityToUV() { - float2 velocity = float2(0.1, 0.0); - bool oob; - - // Left eye bounds [0, 0.5], mapping left eye UV of 0.25 + 0.1 mono velocity -> 0.3 stereo - float2 resultLeft = Stereo::ApplyVelocityToUV(float2(0.25, 0.5), velocity, oob); - ASSERT(IsTrue, abs(resultLeft.x - 0.3) < kEps); - ASSERT(IsTrue, abs(resultLeft.y - 0.5) < kEps); - ASSERT(IsFalse, oob); - - // Right eye bounds [0.5, 1.0], mapping right eye UV of 0.75 + 0.1 mono velocity -> 0.8 stereo - float2 resultRight = Stereo::ApplyVelocityToUV(float2(0.75, 0.5), velocity, oob); - ASSERT(IsTrue, abs(resultRight.x - 0.8) < kEps); - ASSERT(IsTrue, abs(resultRight.y - 0.5) < kEps); - ASSERT(IsFalse, oob); - - // OOB condition: mono velocity pushes past 1.0 - float2 resultOob = Stereo::ApplyVelocityToUV(float2(0.25, 0.5), float2(1.5, 0.0), oob); - ASSERT(IsTrue, oob); - // In VR, out of bounds is clamped (mono x < 0 maps to 0 -> stereo 0, mono x > 1 saturates to 1 -> stereo 0.5 for left) - ASSERT(IsTrue, abs(resultOob.x - 0.5) < kEps); -} diff --git a/package/Shaders/Tests/TestVRFlat.hlsl b/package/Shaders/Tests/TestVRFlat.hlsl deleted file mode 100644 index 7f1e1fd978..0000000000 --- a/package/Shaders/Tests/TestVRFlat.hlsl +++ /dev/null @@ -1,90 +0,0 @@ -// HLSL Unit Tests for Common/VR.hlsli - Flat (non-VR) mode -// Verifies that all Stereo:: functions are correct no-ops / identity when VR is not defined. -// This is the code path most developers exercise. -#define COMPUTESHADER -#include "/Shaders/Common/VR.hlsli" -#include "/Test/STF/ShaderTestFramework.hlsli" - -static const float kEps = 0.0001f; - -/// @tags vr, flat, uv -/// ConvertToStereoUV is identity in flat mode -[numthreads(1, 1, 1)] void TestFlatConvertToStereoUVIsIdentity() { - float2 uv = float2(0.3, 0.7); - float2 result = Stereo::ConvertToStereoUV(uv, 0); - ASSERT(IsTrue, abs(result.x - uv.x) < kEps); - ASSERT(IsTrue, abs(result.y - uv.y) < kEps); - - // Eye index should not matter in flat - result = Stereo::ConvertToStereoUV(uv, 1); - ASSERT(IsTrue, abs(result.x - uv.x) < kEps); - ASSERT(IsTrue, abs(result.y - uv.y) < kEps); - - // invertY param should also be ignored - result = Stereo::ConvertToStereoUV(uv, 0, 1); - ASSERT(IsTrue, abs(result.x - uv.x) < kEps); - ASSERT(IsTrue, abs(result.y - uv.y) < kEps); -} - - /// @tags vr, flat, uv - /// ConvertFromStereoUV is identity in flat mode - [numthreads(1, 1, 1)] void TestFlatConvertFromStereoUVIsIdentity() -{ - float2 uv = float2(0.6, 0.4); - float2 result = Stereo::ConvertFromStereoUV(uv, 0); - ASSERT(IsTrue, abs(result.x - uv.x) < kEps); - ASSERT(IsTrue, abs(result.y - uv.y) < kEps); - - result = Stereo::ConvertFromStereoUV(uv, 1); - ASSERT(IsTrue, abs(result.x - uv.x) < kEps); - ASSERT(IsTrue, abs(result.y - uv.y) < kEps); -} - -/// @tags vr, flat, uv -/// float3/float4 overloads are also identity in flat mode -[numthreads(1, 1, 1)] void TestFlatStereoUVOverloadsAreIdentity() { - float3 uv3 = float3(0.3, 0.7, 0.5); - float3 result3 = Stereo::ConvertToStereoUV(uv3, 0); - ASSERT(IsTrue, abs(result3.x - uv3.x) < kEps); - ASSERT(IsTrue, abs(result3.y - uv3.y) < kEps); - ASSERT(IsTrue, abs(result3.z - uv3.z) < kEps); - - result3 = Stereo::ConvertFromStereoUV(uv3, 1); - ASSERT(IsTrue, abs(result3.x - uv3.x) < kEps); - ASSERT(IsTrue, abs(result3.y - uv3.y) < kEps); - ASSERT(IsTrue, abs(result3.z - uv3.z) < kEps); - - float4 uv4 = float4(0.3, 0.7, 0.5, 1.0); - float4 result4 = Stereo::ConvertToStereoUV(uv4, 0); - ASSERT(IsTrue, abs(result4.x - uv4.x) < kEps); - ASSERT(IsTrue, abs(result4.y - uv4.y) < kEps); - ASSERT(IsTrue, abs(result4.z - uv4.z) < kEps); - ASSERT(IsTrue, abs(result4.w - uv4.w) < kEps); - - float4 result4_from = Stereo::ConvertFromStereoUV(uv4, 1); - ASSERT(IsTrue, abs(result4_from.x - uv4.x) < kEps); - ASSERT(IsTrue, abs(result4_from.y - uv4.y) < kEps); - ASSERT(IsTrue, abs(result4_from.z - uv4.z) < kEps); - ASSERT(IsTrue, abs(result4_from.w - uv4.w) < kEps); -} - - /// @tags vr, flat, uv - /// Round-trip through To/From is identity in flat mode - [numthreads(1, 1, 1)] void TestFlatStereoUVRoundTrip() -{ - float2 uv = float2(0.8, 0.2); - float2 stereo = Stereo::ConvertToStereoUV(uv, 0); - float2 recovered = Stereo::ConvertFromStereoUV(stereo, 0); - ASSERT(IsTrue, abs(recovered.x - uv.x) < kEps); - ASSERT(IsTrue, abs(recovered.y - uv.y) < kEps); -} - -/// @tags vr, flat, uv -/// GetEyeIndexFromTexCoord always returns 0 in flat mode -[numthreads(1, 1, 1)] void TestFlatGetEyeIndexAlwaysZero() { - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.0, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.25, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.5, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.75, 0.5)), 0u); - ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(1.0, 0.5)), 0u); -} diff --git a/package/Shaders/Utility.hlsl b/package/Shaders/Utility.hlsl index 95522d81c9..dc5aeec8fd 100644 --- a/package/Shaders/Utility.hlsl +++ b/package/Shaders/Utility.hlsl @@ -4,8 +4,6 @@ #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" -#include "Common/VR.hlsli" - #if defined(RENDER_SHADOWMASK) || defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) # define RENDER_SHADOWMASK_ANY #endif @@ -29,9 +27,6 @@ struct VS_INPUT float4 BoneWeights: BLENDWEIGHT0; float4 BoneIndices: BLENDINDICES0; #endif -#if defined(VR) - uint InstanceID: SV_INSTANCEID; -#endif // VR }; struct VS_OUTPUT @@ -73,23 +68,13 @@ struct VS_OUTPUT float Depth: TEXCOORD2; # endif #endif -#if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 - uint EyeIndex: EYEIDX0; -#endif // VR }; #ifdef VSHADER cbuffer PerTechnique : register(b0) { -# if !defined(VR) - float4 HighDetailRange[1] : packoffset(c0); // loaded cells center in xy, size in zw + float4 HighDetailRange : packoffset(c0); // loaded cells center in xy, size in zw float2 ParabolaParam : packoffset(c1); // inverse radius in x, y is 1 for forward hemisphere or -1 for backward hemisphere -# else - float4 HighDetailRange[2] : packoffset(c0); // loaded cells center in xy, size in zw - float2 ParabolaParam : packoffset(c2); // inverse radius in x, y is 1 for forward hemisphere or -1 for backward hemisphere -# endif // VR }; cbuffer PerMaterial : register(b1) @@ -99,19 +84,11 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) float4 ShadowFadeParam : packoffset(c0); - row_major float4x4 World[1] : packoffset(c1); - float4 EyePos[1] : packoffset(c5); + row_major float4x4 World : packoffset(c1); + float4 EyePos : packoffset(c5); float4 WaterParams : packoffset(c6); float4 TreeParams : packoffset(c7); -# else - float4 ShadowFadeParam : packoffset(c0); - row_major float4x4 World[2] : packoffset(c1); - float4 EyePos[2] : packoffset(c9); - float4 WaterParams : packoffset(c11); - float4 TreeParams : packoffset(c12); -# endif // VR }; float2 SmoothSaturate(float2 value) @@ -123,18 +100,12 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif - ); - # if (defined(RENDER_DEPTH) && defined(RENDER_SHADOWMASK_ANY)) || SHADOWFILTER == 2 vsout.PositionCS.xy = input.PositionMS.xy; # if defined(RENDER_SHADOWMASKDPB) || defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) vsout.PositionCS.z = ShadowFadeParam.z; # else - vsout.PositionCS.z = HighDetailRange[eyeIndex].x; + vsout.PositionCS.z = HighDetailRange.x; # endif vsout.PositionCS.w = 1; # elif defined(STENCIL_ABOVE_WATER) @@ -157,18 +128,18 @@ VS_OUTPUT main(VS_INPUT input) # endif # if defined(LOD_LANDSCAPE) - positionMS = LodLandscape::AdjustLodLandscapeVertexPositionMS(positionMS, World[eyeIndex], HighDetailRange[eyeIndex]); + positionMS = LodLandscape::AdjustLodLandscapeVertexPositionMS(positionMS, World, HighDetailRange); # endif # if defined(SKINNED) precise int4 boneIndices = 765.01.xxxx * input.BoneIndices.xyzw; - float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, boneIndices, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, input.BoneWeights); + float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, boneIndices, FrameBuffer::CameraPosAdjust.xyz, input.BoneWeights); precise float4 positionWS = float4(mul(positionMS, transpose(worldMatrix)), 1); - positionCS = mul(FrameBuffer::CameraViewProj[eyeIndex], positionWS); + positionCS = mul(FrameBuffer::CameraViewProj, positionWS); # else - precise float4x4 modelViewProj = mul(FrameBuffer::CameraViewProj[eyeIndex], World[eyeIndex]); + precise float4x4 modelViewProj = mul(FrameBuffer::CameraViewProj, World); positionCS = mul(modelViewProj, positionMS); # endif @@ -193,9 +164,9 @@ VS_OUTPUT main(VS_INPUT input) # if defined(SKINNED) float3x3 boneRSMatrix = Skinned::GetBoneRSMatrix(Bones, boneIndices, input.BoneWeights); normalMS = normalize(mul(normalMS, transpose(boneRSMatrix))); - normalVS = mul(FrameBuffer::CameraView[eyeIndex], float4(normalMS, 0)).xyz; + normalVS = mul(FrameBuffer::CameraView, float4(normalMS, 0)).xyz; # else - normalVS = mul(mul(FrameBuffer::CameraView[eyeIndex], World[eyeIndex]), float4(normalMS, 0)).xyz; + normalVS = mul(mul(FrameBuffer::CameraView, World), float4(normalMS, 0)).xyz; # endif # if defined(RENDER_NORMAL_CLAMP) normalVS = max(min(normalVS, 0.1), -0.1); @@ -220,12 +191,12 @@ VS_OUTPUT main(VS_INPUT input) float falloff = 1; # if defined(RENDER_NORMAL_FALLOFF) # if defined(SKINNED) - falloff = dot(normalMS, normalize(EyePos[eyeIndex].xyz - positionWS.xyz)); + falloff = dot(normalMS, normalize(EyePos.xyz - positionWS.xyz)); # else - falloff = dot(normalMS, normalize(EyePos[eyeIndex].xyz - positionMS.xyz)); + falloff = dot(normalMS, normalize(EyePos.xyz - positionMS.xyz)); # endif # endif - texCoord.w = EyePos[eyeIndex].w * falloff; + texCoord.w = EyePos.w * falloff; # endif vsout.TexCoord0 = texCoord; @@ -273,13 +244,6 @@ VS_OUTPUT main(VS_INPUT input) vsout.PositionCS.z += 5.0; # endif -# ifdef VR - vsout.EyeIndex = eyeIndex; - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.PositionCS, eyeIndex); - vsout.PositionCS = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR return vsout; } #endif @@ -332,37 +296,18 @@ cbuffer PerGeometry : register(b2) float4 PropertyColor : packoffset(c1); float4 AlphaTestRef : packoffset(c2); float4 ShadowLightParam : packoffset(c3); // Falloff in x, ShadowDistance squared in z -# if !defined(VR) float4x3 FocusShadowMapProj[4] : packoffset(c4); -# if defined(RENDER_SHADOWMASK) - float4x3 ShadowMapProj[1][3] : packoffset(c16); // 16, 19, 22 -# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) - float4x4 ShadowMapProj[1][3] : packoffset(c16); -# endif -# else - float4 VRUnknown : packoffset(c4); // used to multiply by identity matrix, see e.g., 4202499.ps.bin.hlsl - /* - r1.x = dot(cb2[4].xz, icb[r0.w+0].xz); - r1.x = r0.x * cb12[86].x + -r1.x; - r0.w = (int)r0.w + 1; - r0.w = (int)r0.w + -1; - r0.w = dot(cb2[4].yw, icb[r0.w+0].xz); - */ - float4x3 FocusShadowMapProj[4] : packoffset(c5); -# if defined(RENDER_SHADOWMASK) - float4x3 ShadowMapProj[2][3] : packoffset(c29); // VR has a couple of offsets of 3, e.g., {29, 32, 35} and {38, 41, 44}, compare to Flat which does [16, 19, 22] -# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) - float4x4 ShadowMapProj[2][3] : packoffset(c29); -# endif -# endif // VR +# if defined(RENDER_SHADOWMASK) + float4x3 ShadowMapProj[3] : packoffset(c16); // 16, 19, 22 +# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) + float4x4 ShadowMapProj[3] : packoffset(c16); +# endif } -# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } -# endif // !VR float SampleShadowPCF(Texture2DArray tex, SamplerComparisonState samp, float2 baseUV, float layerIndex, float compareValue, float2x2 rotationMatrix, float radius) { @@ -392,11 +337,6 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; -# if !defined(VR) - uint eyeIndex = 0; -# else - uint eyeIndex = input.EyeIndex; -# endif // !VR # if defined(ADDITIONAL_ALPHA_MASK) uint2 alphaMask = input.PositionCS.xy; alphaMask.x = ((alphaMask.x << 2) & 12); @@ -488,9 +428,9 @@ PS_OUTPUT main(PS_INPUT input) TexStencilSampler.GetDimensions(0, stencilDimensions.x, stencilDimensions.y, stencilDimensions.z); stencilValue = TexStencilSampler.Load(float3(stencilDimensions.xy * depthUv, 0)).x; # endif - depthUv = Stereo::ConvertFromStereoUV(depthUv * FrameBuffer::DynamicResolutionParams2.xy, eyeIndex); + depthUv = depthUv * FrameBuffer::DynamicResolutionParams2.xy; float4 positionCS = float4(2 * float2(depthUv.x, -depthUv.y + 1) - 1, depth, 1); - float4 positionMS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionCS); + float4 positionMS = mul(FrameBuffer::CameraViewProjInverse, positionCS); positionMS.xyz = positionMS.xyz / positionMS.w; float fadeFactor = 1 - pow(saturate(dot(positionMS.xyz, positionMS.xyz) / ShadowLightParam.z), 8); @@ -513,15 +453,15 @@ PS_OUTPUT main(PS_INPUT input) shadowColor = float4(0, 0, 0, 0); if (EndSplitDistances.z >= shadowMapDepth) { - float4x3 lightProjectionMatrix = ShadowMapProj[eyeIndex][0]; + float4x3 lightProjectionMatrix = ShadowMapProj[0]; float shadowMapThreshold = AlphaTestRef.y; float cascadeIndex = 0; if (2.5 < EndSplitDistances.w && EndSplitDistances.y < shadowMapDepth) { - lightProjectionMatrix = ShadowMapProj[eyeIndex][2]; + lightProjectionMatrix = ShadowMapProj[2]; shadowMapThreshold = AlphaTestRef.z; cascadeIndex = 2; } else if (EndSplitDistances.x < shadowMapDepth) { - lightProjectionMatrix = ShadowMapProj[eyeIndex][1]; + lightProjectionMatrix = ShadowMapProj[1]; shadowMapThreshold = AlphaTestRef.z; cascadeIndex = 1; } @@ -544,7 +484,7 @@ PS_OUTPUT main(PS_INPUT input) if (cascadeIndex < 1 && StartSplitDistances.y < shadowMapDepth) { float cascade1ShadowVisibility = 0; - float3 cascade1PositionLS = mul(transpose(ShadowMapProj[eyeIndex][1]), float4(positionMS.xyz, 1)).xyz; + float3 cascade1PositionLS = mul(transpose(ShadowMapProj[1]), float4(positionMS.xyz, 1)).xyz; # if SHADOWFILTER == 0 float cascade1ShadowMapValue = TexShadowMapSampler.Sample(SampShadowMapSampler, float3(cascade1PositionLS.xy, 1)).x; @@ -580,7 +520,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = lerp(1.0 * !SharedData::InInterior, shadowVisibility, fadeFactor); } # elif defined(RENDER_SHADOWMASKSPOT) - float4 positionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)); + float4 positionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)); positionLS.xyz /= positionLS.w; float2 shadowMapUv = positionLS.xy * 0.5 + 0.5; float shadowBaseVisibility = 0; @@ -618,7 +558,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = fadeFactor * shadowVisibility; # elif defined(RENDER_SHADOWMASKPB) - float4 unadjustedPositionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)); + float4 unadjustedPositionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)); float shadowVisibility = 0; @@ -643,7 +583,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = fadeFactor * shadowVisibility; # elif defined(RENDER_SHADOWMASKDPB) - float3 positionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)).xyz; + float3 positionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)).xyz; bool lowerHalf = positionLS.z * 0.5 + 0.5 < 0; float3 normalizedPositionLS = normalize(positionLS); diff --git a/package/Shaders/VR/InSceneOverlay.ps.hlsl b/package/Shaders/VR/InSceneOverlay.ps.hlsl deleted file mode 100644 index 7638542c53..0000000000 --- a/package/Shaders/VR/InSceneOverlay.ps.hlsl +++ /dev/null @@ -1,18 +0,0 @@ -// VR In-Scene Overlay Pixel Shader -// Samples overlay texture with alpha blending support - -Texture2D shaderTexture : register(t0); -SamplerState sampleType : register(s0); - -struct PS_INPUT -{ - float4 pos: SV_POSITION; - float2 uv: TEXCOORD0; -}; - -float4 main(PS_INPUT input) : - SV_TARGET -{ - float4 color = shaderTexture.Sample(sampleType, input.uv); - return color; -} diff --git a/package/Shaders/VR/InSceneOverlay.vs.hlsl b/package/Shaders/VR/InSceneOverlay.vs.hlsl deleted file mode 100644 index b8d2d09308..0000000000 --- a/package/Shaders/VR/InSceneOverlay.vs.hlsl +++ /dev/null @@ -1,27 +0,0 @@ -// VR In-Scene Overlay Vertex Shader -// Simple pass-through shader for rendering overlay quad in VR - -cbuffer MatrixBuffer : register(b0) -{ - matrix wvp; -}; - -struct VS_INPUT -{ - float3 pos: POSITION; - float2 uv: TEXCOORD0; -}; - -struct PS_INPUT -{ - float4 pos: SV_POSITION; - float2 uv: TEXCOORD0; -}; - -PS_INPUT main(VS_INPUT input) -{ - PS_INPUT output; - output.pos = mul(float4(input.pos, 1.0f), wvp); - output.uv = input.uv; - return output; -} diff --git a/package/Shaders/VR/StereoBlendCS.hlsl b/package/Shaders/VR/StereoBlendCS.hlsl deleted file mode 100644 index a6e66d1ea0..0000000000 --- a/package/Shaders/VR/StereoBlendCS.hlsl +++ /dev/null @@ -1,362 +0,0 @@ -// Stereo Bilateral Blend - Post-composite stereo consistency pass for VR -// -// Full-image depth-aware bilateral blend with back-check validation that -// reprojects each pixel to the other eye and blends based on depth agreement. -// Source and destination edge detection guard silhouette boundaries before -// reprojection; the back-check provides a second layer of validation. -// -// Based on the stereo-aware bilateral filter from: -// Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" -// https://eprints.whiterose.ac.uk/id/eprint/187713/ - -#include "Common/Color.hlsli" -#include "Common/FrameBuffer.hlsli" -#include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" - -Texture2D ColorTexture : register(t0); -Texture2D DepthTexture : register(t1); - -RWTexture2D OutputRW : register(u0); - -#ifdef STEREO_OVERWRITE -RWTexture2D MotionRW : register(u1); -Texture2D ModeTexture : register(t2); -Texture2D PomOffsetTexture : register(t3); // R16_FLOAT: Stereo::POM_NO_DATA (-1.0) = no POM; >= 0.0 = POM ran -SamplerState LinearSampler : register(s0); - -# include "VRStereoOptimizations/modes.hlsli" - -// Hardware bilinear color sample from reprojected pixel coordinates. -// Converts integer pixel coords to proper full-texture UV for SampleLevel, -// clamped to the active DRS viewport to prevent sampling stale data. -// Motion vectors stay as integer Load() — filtering them breaks DLSS. -float4 SampleReprojectedColor(float2 stereoUV, float2 frameDim) -{ - uint texW, texH; - ColorTexture.GetDimensions(texW, texH); - float2 texSize = float2(texW, texH); - float2 minUV = 0.5 / texSize; - float2 maxUV = (frameDim - 0.5) / texSize; - stereoUV = clamp(stereoUV, minUV, maxUV); - return ColorTexture.SampleLevel(LinearSampler, stereoUV, 0); -} -#endif - -cbuffer StereoBlendCB : register(b1) -{ - float2 FrameDim; - float2 RcpFrameDim; - float DepthSigma; - float MaxBlendFactor; - float ColorDiffThreshold; - float DebugEdgeTint; - uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer, 3 = POM depth heatmap - float FullBlendDistance; - float POMDepthScale; - float _pad; -}; - -static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo blend -static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check -static const float kDepthAgreementThreshold = 0.015; // Relative depth difference threshold for overwrite mode disocclusion rejection - -// Samples four depth neighbors in a cross pattern (±offset pixels) around center, -// clamped to eyeIndex's half of the packed stereo buffer to avoid seam contamination. -float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) -{ - return float4( - DepthTexture[Stereo::ClampToEyeBounds(center + int2(offset, 0), eyeIndex, FrameDim)], - DepthTexture[Stereo::ClampToEyeBounds(center + int2(-offset, 0), eyeIndex, FrameDim)], - DepthTexture[Stereo::ClampToEyeBounds(center + int2(0, offset), eyeIndex, FrameDim)], - DepthTexture[Stereo::ClampToEyeBounds(center + int2(0, -offset), eyeIndex, FrameDim)]); -} - -[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { - if (any(dtid >= uint2(FrameDim))) - return; - -#ifdef STEREO_OVERWRITE - // ========================================================================= - // Mode-driven stereo merge: reads per-pixel classification from StencilCS - // and applies appropriate action per mode and eye. - // Mode texture is full SBS resolution — ModeTexture[dtid] maps directly. - // ========================================================================= - - float2 uv = (dtid + 0.5) * RcpFrameDim; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - float centerDepth = DepthTexture[dtid]; - - // HMD mask pixels (depth >= 1.0 in reversed-Z) — always skip - if (centerDepth >= 1.0) - return; - - uint pixelMode = ModeTexture[dtid]; - - // Debug mode 1: depth map diagnostic — show mode texture as solid colors (all pixels) - if (DebugMode == 1) { - float4 c = ColorTexture[dtid]; - if (pixelMode == MODE_EDGE) - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); - else if (pixelMode == MODE_EDGE_NEIGHBOUR) - OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); - else if (pixelMode == MODE_DISOCCLUDED) - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); - else if (pixelMode == MODE_FULL_BLEND) - OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); - return; - } - - // Debug mode 2: full blend depth visualizer — cyan tint based on proximity to FullBlendDistance - if (DebugMode == 2) { - if (centerDepth < 1e-5 || centerDepth >= 1.0) - return; - float linDepth = SharedData::GetScreenDepth(centerDepth); - if (linDepth < FullBlendDistance) { - float4 c = ColorTexture[dtid]; - float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); - } - return; - } - - // Debug mode 3: POM depth data visualizer — show PomOffsetTexture as color - if (DebugMode == 3) { - float pomVal = PomOffsetTexture[dtid]; - float4 c = ColorTexture[dtid]; - if (pomVal >= 0.0) { - // POM pixel: red-to-green gradient based on parallaxAmount - // Red = peak (high pomVal, closer to camera), Green = valley (low pomVal, farther), Yellow = geometry plane - float3 pomColor = float3(pomVal, 1.0 - pomVal, 0); - OutputRW[dtid] = float4(lerp(c.rgb, pomColor, 0.7), c.a); - } - // Non-POM pixels store -1.0 sentinel, left untouched - return; - } - - // MODE_DISOCCLUDED: fully shaded, leave untouched - if (pixelMode == MODE_DISOCCLUDED) - return; - - // MODE_FULL_BLEND: bilateral blend for 2x supersampling - if (pixelMode == MODE_FULL_BLEND) { - float4 center = ColorTexture[dtid]; - - // Check for POM depth offset at this pixel - // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. - // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). - // Correction: high pomVal should push depth closer (smaller linear depth), - // so we use (0.5 - pomOffset) to get a negative correction for peaks. - // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. - float reprojDepthFB = centerDepth; - float pomOffsetFB = PomOffsetTexture[dtid]; - if (pomOffsetFB >= 0.0 && POMDepthScale > 0) { - float linDepthFB = SharedData::GetScreenDepth(centerDepth); - float depthCorrectionFB = (0.5 - pomOffsetFB) * POMDepthScale; - float newLinDepthFB = max(linDepthFB + depthCorrectionFB, 1e-4); - reprojDepthFB = (SharedData::CameraData.x - SharedData::CameraData.w / newLinDepthFB) / SharedData::CameraData.z; - } - - // Reproject to the other eye - Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepthFB, eyeIndex, FrameDim); - if (!r.valid) { - // Debug tint for failed reprojection - if (DebugEdgeTint > 0) - OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); - return; - } - - // Only blend with pixels that have valid composited data in both eyes - uint otherMode = ModeTexture[r.otherPx]; - if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) - return; - - float4 otherColor = SampleReprojectedColor(r.otherStereoUV, FrameDim); - float otherDepth = DepthTexture[r.otherPx]; - - // Depth-weighted bilateral blend - float maxDepth = max(max(centerDepth, otherDepth), 1e-5); - float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); - float blendWeight = 0.5 * depthAgreement; - - float4 result = lerp(center, otherColor, blendWeight); - - if (DebugEdgeTint > 0) - result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); - - OutputRW[dtid] = result; - return; - } - - if (eyeIndex == 0) { - // Eye 0 (left eye): fully shaded for all modes — only apply debug tint to edge pixels - if (DebugEdgeTint > 0 && pixelMode == MODE_EDGE) { - float4 c = ColorTexture[dtid]; - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), DebugEdgeTint), c.a); - } - return; - } - - // Eye 1 (right eye): reproject all non-disoccluded, non-full-blend pixels - // (MAIN, EDGE) from Eye 0 (left eye). In VR stereo rendering, Eye 0 is - // fully shaded; Eye 1 pixels marked as reprojectable by StencilCS are - // filled with reprojected color from Eye 0 to save GPU work. - // StencilCS already performed the authoritative disocclusion check with the correct - // depth buffer state — no redundant depth agreement check here. - float reprojDepth = centerDepth; - - // First-pass reprojection to find Eye 0 source pixel - Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); - if (!r.valid) - return; - - // Save first-pass result as fallback before POM adjustment - Stereo::StereoBilateralResult firstPassR = r; - - // Read POM offset from dedicated POM texture (R16_FLOAT, written by Lighting PS at u7). - // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. - // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). - // Correction: high pomOffset should push depth closer (smaller linear depth), - // so we use (0.5 - pomOffset) to get a negative correction for peaks. - // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. - float pomOffset = PomOffsetTexture[r.otherPx]; - if (pomOffset >= 0.0) { - // Re-reproject with POM-adjusted depth centered at geometry plane - float linearDepth = SharedData::GetScreenDepth(centerDepth); - float depthCorrection = (0.5 - pomOffset) * POMDepthScale; - float newLinearDepth = max(linearDepth + depthCorrection, 1e-4); - reprojDepth = (SharedData::CameraData.x - SharedData::CameraData.w / newLinearDepth) / SharedData::CameraData.z; - r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); - if (!r.valid) - r = firstPassR; // Fall back to non-POM reprojection - } - - // Skip if the Eye 0 source pixel is sky/unrendered (depth at clear value). - // At DeferredPasses time, sky hasn't rendered yet — source would have clear color. - // Let the sky/water pass fill these pixels later instead. - float sourceDepth = DepthTexture[r.otherPx]; - if (sourceDepth >= 1.0 || sourceDepth < 1e-5) { - // POM adjustment landed on sky — try the original first-pass source - if (r.otherPx.x != firstPassR.otherPx.x || r.otherPx.y != firstPassR.otherPx.y) { - float fallbackDepth = DepthTexture[firstPassR.otherPx]; - if (fallbackDepth < 1.0 && fallbackDepth >= 1e-5) { - r = firstPassR; - } else { - return; - } - } else { - return; - } - } - - OutputRW[dtid] = SampleReprojectedColor(r.otherStereoUV, FrameDim); - MotionRW[dtid] = MotionRW[r.otherPx]; - -#else // Normal bilateral blend path - -# ifdef EYE0_ONLY - // Only process Eye 0 (left half) - Eye 1 left untouched - float2 uvCheck = (dtid + 0.5) * RcpFrameDim; - if (Stereo::GetEyeIndexFromTexCoord(uvCheck) == 1) - return; -# endif - - float2 uv = (dtid + 0.5) * RcpFrameDim; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - float4 centerColor = ColorTexture[dtid]; - float centerDepth = DepthTexture[dtid]; - - // Debug states: - // 0 = mask/sky: skipped (depth == 0 or 1) - // 1 = source edge: depth discontinuity at this pixel - // 2 = destination edge: depth discontinuity at reprojected pixel - // 3 = out of bounds: reprojection left the other eye's frame - // 4 = blended, back-check passed: surfaces match in both eyes - // 5 = blended, back-check failed: blend penalized (occlusion edge) - uint debugState = 0; - - Stereo::StereoBilateralResult r = (Stereo::StereoBilateralResult)0; - float4 blendedColor = centerColor; - - // depth == 0.0: VR HMD mask (pixels outside the lens area, never written by the engine) - // depth == 1.0: sky/far plane (no real geometry, bilateral reprojection not meaningful) - bool isSkipPixel = centerDepth < 1e-5 || centerDepth >= 1.0; - if (!isSkipPixel) { - // Source edge detection: skip at depth discontinuities (arm/world silhouettes, - // object edges). Saves VP reprojection work and prevents halo artifacts. - float4 srcEdgeDepths = SampleCrossDepths(dtid, 1, eyeIndex); - if (Stereo::MaxDepthDiff(centerDepth, srcEdgeDepths) > kEdgeDepthThreshold) { - debugState = 1; - } else { - r = Stereo::ReprojectToOtherEye(uv, centerDepth, eyeIndex, FrameDim); - if (r.valid) { - float otherDepth = DepthTexture[r.otherPx]; - - float4 dstEdgeDepths = SampleCrossDepths(r.otherPx, kEdgeMargin, 1 - eyeIndex); - if (any(dstEdgeDepths < 1e-5) || Stereo::MaxDepthDiff(otherDepth, dstEdgeDepths) > kEdgeDepthThreshold) { - debugState = 2; - } else { - float4 otherColor = ColorTexture[r.otherPx]; - Stereo::FinalizeStereoBlend(r, uv, centerDepth, otherDepth, eyeIndex, FrameDim, DepthSigma, MaxBlendFactor); - - float colorDiff = abs(dot(centerColor.rgb, float3(0.2126, 0.7152, 0.0722)) - - dot(otherColor.rgb, float3(0.2126, 0.7152, 0.0722))); - float colorGate = smoothstep(ColorDiffThreshold * 0.5, ColorDiffThreshold * 2.0, colorDiff); - r.blendWeight *= colorGate; - - blendedColor = lerp(centerColor, otherColor, r.blendWeight); - debugState = r.backCheckPassed ? 4 : 5; - } - } else { - debugState = 3; - } - } - } - -# ifdef DEBUG_BACKCHECK - // Debug visualization (6 states): - // Blue = mask/sky: skipped - // Yellow = source edge: depth discontinuity at this pixel - // Orange = destination edge: depth discontinuity at reprojected pixel - // Grey = out of bounds: other eye can't see this point - // Green = back-check passed: surfaces match in both eyes - // Red = back-check failed: blend penalized (occlusion edge) - float3 debugColors[6] = { - float3(0.1, 0.1, 0.5), // 0: mask/sky - blue - float3(0.8, 0.8, 0.0), // 1: source edge - yellow - float3(0.8, 0.4, 0.0), // 2: destination edge - orange - float3(0.3, 0.3, 0.3), // 3: out of bounds - grey - float3(0.0, 0.5, 0.0), // 4: back-check passed - green - float3(0.5, 0.0, 0.0) // 5: back-check failed - red - }; - OutputRW[dtid] = float4(lerp(centerColor.rgb, debugColors[debugState], 0.7), centerColor.a); -# elif defined(DEBUG_BLEND_WEIGHT) - // Blend weight heatmap: only pixels with actual blend activity are colorized. - // Untouched pixels pass through unmodified. - float w = saturate(r.blendWeight / max(MaxBlendFactor, 1e-5)); - if (w > 1e-3) { - float3 heatmap = Color::TurboColormap(w); - OutputRW[dtid] = float4(lerp(centerColor.rgb, saturate(heatmap), 0.8), centerColor.a); - } else { - OutputRW[dtid] = centerColor; - } -# elif defined(DEBUG_EDGE_DETECTION) - // Edge detection visualizer: highlights pixels excluded by depth discontinuity checks. - // Non-edge pixels show the normal blended output for scene context. - // Bright yellow = source edge: discontinuity at this pixel - // Bright orange = destination edge: discontinuity at reprojected pixel - if (debugState == 1) { - OutputRW[dtid] = float4(lerp(centerColor.rgb, float3(1.0, 1.0, 0.0), 0.8), centerColor.a); - } else if (debugState == 2) { - OutputRW[dtid] = float4(lerp(centerColor.rgb, float3(1.0, 0.5, 0.0), 0.8), centerColor.a); - } else { - OutputRW[dtid] = blendedColor; - } -# else - OutputRW[dtid] = blendedColor; -# endif - -#endif // STEREO_OVERWRITE -} diff --git a/package/Shaders/VR/VRPostProcessCS.hlsl b/package/Shaders/VR/VRPostProcessCS.hlsl deleted file mode 100644 index 770e244553..0000000000 --- a/package/Shaders/VR/VRPostProcessCS.hlsl +++ /dev/null @@ -1,109 +0,0 @@ -// VR Post-Process - Bilateral blend for near-camera 2x supersampling -// -// Runs after all compositing and stereo blending is complete. -// Reads per-pixel classification from StencilCS and applies: -// - MODE_FULL_BLEND: bilateral depth-weighted blend for 2x supersampling -// -// Only MODE_FULL_BLEND pixels are processed. All others pass through untouched. - -#include "Common/FrameBuffer.hlsli" -#include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" - -Texture2D ColorTexture : register(t0); // Copy of final composited image -Texture2D ModeTexture : register(t1); -Texture2D DepthTexture : register(t2); - -RWTexture2D OutputRW : register(u0); - -cbuffer VRPostProcessCB : register(b1) -{ - float2 FrameDim; - float2 RcpFrameDim; - float DebugEdgeTint; // 0 = off, >0 = debug visualization strength - uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer - float FullBlendDistance; // Linearized depth threshold for full blend zone visualization - float _pad; // Pad to 16-byte alignment -}; - -#include "VRStereoOptimizations/modes.hlsli" - -[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { - if (any(dtid >= uint2(FrameDim))) - return; - - uint pixelMode = ModeTexture[dtid]; - - // Depth map diagnostic: show mode texture contents as solid colors - if (DebugMode == 1) { - float4 c = ColorTexture[dtid]; - if (pixelMode == MODE_EDGE) - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); - else if (pixelMode == MODE_EDGE_NEIGHBOUR) - OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); - else if (pixelMode == MODE_DISOCCLUDED) - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); - else if (pixelMode == MODE_FULL_BLEND) - OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); // Orange = full blend zone - return; - } - - // Full blend depth visualizer: shows the depth boundary as a cyan tint - if (DebugMode == 2) { - float2 uvDb = (dtid + 0.5) * RcpFrameDim; - float depthDb = DepthTexture[dtid]; - if (depthDb < 1e-5 || depthDb >= 1.0) - return; - float linDepth = SharedData::GetScreenDepth(depthDb); - if (linDepth < FullBlendDistance) { - float4 c = ColorTexture[dtid]; - float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); - OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); - } - return; - } - - // Only process full blend pixels - if (pixelMode != MODE_FULL_BLEND) - return; - - float2 uv = (dtid + 0.5) * RcpFrameDim; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - float4 result = ColorTexture[dtid]; - - // === MODE_FULL_BLEND: bilateral blend for 2x supersampling === - { - float4 center = result; - float centerDepth = DepthTexture[dtid]; - - // Reproject to the other eye - Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, centerDepth, eyeIndex, FrameDim); - if (!r.valid) { - // Debug tint for failed reprojection - if (DebugEdgeTint > 0) - OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); - return; - } - - // Only blend with pixels that have valid composited data in both eyes. - uint otherMode = ModeTexture[r.otherPx]; - if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) - return; - - float4 otherColor = ColorTexture[r.otherPx]; - float otherDepth = DepthTexture[r.otherPx]; - - // Depth-weighted bilateral blend - float maxDepth = max(max(centerDepth, otherDepth), 1e-5); - float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); - float blendWeight = 0.5 * depthAgreement; - - result = lerp(center, otherColor, blendWeight); - - if (DebugEdgeTint > 0) - result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); - } - - OutputRW[dtid] = result; -} diff --git a/package/Shaders/VRStereoOptimizations/StencilCS.hlsl b/package/Shaders/VRStereoOptimizations/StencilCS.hlsl deleted file mode 100644 index 8a66a7e676..0000000000 --- a/package/Shaders/VRStereoOptimizations/StencilCS.hlsl +++ /dev/null @@ -1,172 +0,0 @@ -// VR Stereo Optimizations - Stencil Classification Compute Shader -// -// Classifies BOTH eyes over the full SBS buffer. Each pixel is tagged as: -// MODE_DISOCCLUDED - Must be fully shaded (sky, HMD mask, parallax-occluded) -// MODE_EDGE - Depth edge boundary (dist 1) or inner/foreground band; fully shaded + bilateral blend -// MODE_MAIN - Standard pixel eligible for reprojection / bilateral blend -// MODE_FULL_BLEND - Near-camera geometry: both eyes fully shaded for 2x supersampling -// -// Dispatched over full SBS resolution (FrameDim.x x FrameDim.y). - -#include "Common/SharedData.hlsli" -#include "Common/VR.hlsli" -#include "VRStereoOptimizations/cbuffers.hlsli" - -Texture2D DepthTexture : register(t0); - -RWTexture2D ModeTextureRW : register(u0); - -// Sentinel for the edge-detection search: means "no discontinuity found yet". -static const uint kEdgeDistNone = 0xFFFFFFFFu; - -[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { - if (any(dtid >= uint2(FrameDim))) - return; - - // Determine which eye this pixel belongs to - float2 uv = (float2(dtid) + 0.5) / FrameDim; - uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); - - // Read depth directly in SBS coords - float centerDepth = DepthTexture[dtid]; - -#ifdef DEBUG_DEPTH_MAP - // DIAGNOSTIC: Visualize what depth values StencilCS sees. - // Green (MODE_EDGE) = depth >= 1.0 (HMD mask threshold) - // Magenta (MODE_EDGE_NEIGHBOUR) = depth < EPSILON_DEPTH_SKY (sky threshold) - // No tint (MODE_MAIN) = normal geometry with valid depth - if (centerDepth >= 1.0) { - ModeTextureRW[dtid] = MODE_EDGE; - return; - } - if (centerDepth < EPSILON_DEPTH_SKY) { - ModeTextureRW[dtid] = MODE_EDGE_NEIGHBOUR; - return; - } - ModeTextureRW[dtid] = MODE_MAIN; - return; -#endif - - // Sky/unrendered pixels (depth >= 1.0 at z-prepass time = depth buffer clear value) - // and HMD mask pixels both have depth >= 1.0 here. Treat them the same as sky: - // let edge detection run so geometry-vs-sky boundaries get classified. - // HMD mask pixels are in lens corners with no nearby geometry, so they'll - // fall through to MODE_DISOCCLUDED at the end. - bool isSky = (centerDepth < EPSILON_DEPTH_SKY) || (centerDepth >= 1.0); - float linCenter = isSky ? DEPTH_SKY_SENTINEL : SharedData::GetScreenDepth(centerDepth); - - // Near-camera supersampling: geometry closer than FullBlendDistance gets full - // shading in both eyes for bilateral blend (2x supersampling in VRPostProcess). - if (!isSky && linCenter < FullBlendDistance) { - ModeTextureRW[dtid] = MODE_FULL_BLEND; - return; - } - - // --- Disocclusion detection via reprojection (runs for all non-sky pixels) --- - // Early return: disoccluded pixels are always MODE_DISOCCLUDED regardless of edge proximity. - // This ensures MinEdgeDistance never affects disocclusion classification. - if (!isSky) { - Stereo::StereoBilateralResult reproj = Stereo::ReprojectToOtherEye( - uv, - centerDepth, - eyeIndex, - FrameDim); - - bool isDisoccluded = false; - if (!reproj.valid) { - isDisoccluded = true; - } else { - float otherDepth = DepthTexture[reproj.otherPx]; - // Raw reversed-Z depth comparison for disocclusion detection. - // Using raw depth avoids concentric semicircle artifacts that occur - // with linearized depth due to precision band boundaries in the - // hyperbolic depth-to-linear conversion. - float maxRaw = max(max(centerDepth, otherDepth), EPSILON_DIVISION); - float rawRelDiff = abs(centerDepth - otherDepth) / maxRaw; - isDisoccluded = (rawRelDiff > DisocclusionThreshold); - - // Directional disocclusion: catches silhouette edge pixels where both eyes sample - // similar linearized depth but Eye 0's color is wrong for Eye 1. These slip through - // the symmetric rawRelDiff check above. The condition fires when Eye 0 is at similar - // or slightly closer depth than Eye 1 (scale < 1.0), marking them disoccluded so Eye 1 - // renders natively. ForwardOcclusionScale=0.5 triggers when Eye 0 is less than 2x Eye 1's - // linearized depth; lower values are more aggressive, 0 = disabled. - if (!isDisoccluded && eyeIndex == 1 && ForwardOcclusionScale > 0.0) { - bool otherIsSky = (otherDepth < EPSILON_DEPTH_SKY) || (otherDepth >= 1.0); - if (!otherIsSky) { - float linOther = SharedData::GetScreenDepth(otherDepth); - isDisoccluded = (linOther * ForwardOcclusionScale < linCenter); - } - } - } - - if (isDisoccluded) { - ModeTextureRW[dtid] = MODE_DISOCCLUDED; - return; - } - } - - // Depth gate: skip edge detection for nearby geometry (saves perf, distant AA matters more) - // Sky pixels always run edge detection — they need to expand the edge band outward. - // Disocclusion detection (above) is independent of this gate and always runs. - bool skipEdgeDetection = !isSky && (linCenter < MinEdgeDistance); - - // --- Edge detection with two-tier classification --- - // MODE_EDGE: immediate neighbor (distance 1) has depth discontinuity, OR - // inner/foreground band (distance <= kInnerWidth). - // kInnerWidth=4 provides enough margin at high VR resolutions (~8k wide) to catch - // disocclusion boundary pixels that are just outside the immediate-neighbor band. - static const uint kInnerWidth = 4; - int2 offsets[4] = { int2(-1, 0), int2(1, 0), int2(0, -1), int2(0, 1) }; - - uint nearestEdgeDist = kEdgeDistNone; // nearest distance at which a discontinuity was found - bool nearestWeAreOuter = false; // whether we are on the background side at that nearest hit - - // Use the larger of inner/outer widths for the search - uint maxWidth = kInnerWidth; - - if (!skipEdgeDetection) { - [loop] for (uint d = 1; d <= maxWidth; d++) - { - [unroll] for (int i = 0; i < 4; i++) - { - int2 rawNeighbor = int2(dtid) + offsets[i] * (int)d; - uint2 neighborCoord = Stereo::ClampToEyeBounds(rawNeighbor, eyeIndex, FrameDim); - - float neighborDepth = DepthTexture[neighborCoord]; - bool neighborIsSky = (neighborDepth < EPSILON_DEPTH_SKY) || (neighborDepth >= 1.0); - float linNeighbor = neighborIsSky ? DEPTH_SKY_SENTINEL : SharedData::GetScreenDepth(neighborDepth); - float maxLin = max(max(linCenter, linNeighbor), EPSILON_DEPTH_SKY); - float relDepthDiff = abs(linCenter - linNeighbor) / maxLin; - - if (relDepthDiff > EdgeDepthThreshold && d < nearestEdgeDist) { - nearestEdgeDist = d; - nearestWeAreOuter = (linNeighbor < linCenter); // neighbor closer to camera = we are background - } - } - } - - } // !skipEdgeDetection - - if (nearestEdgeDist != kEdgeDistNone) { - // Classify based on distance and side - if (nearestEdgeDist == 1) { - // Immediate neighbor discontinuity: always MODE_EDGE regardless of side - ModeTextureRW[dtid] = MODE_EDGE; - return; - } else if (!nearestWeAreOuter && nearestEdgeDist <= kInnerWidth) { - // Inner/foreground band beyond distance 1 - ModeTextureRW[dtid] = MODE_EDGE; - return; - } - } - - // Sky pixels that aren't near edges -> disoccluded (reprojection is meaningless for sky) - if (isSky) { - ModeTextureRW[dtid] = MODE_DISOCCLUDED; - return; - } - - // Standard pixel - ModeTextureRW[dtid] = MODE_MAIN; -} diff --git a/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl deleted file mode 100644 index 6e49007035..0000000000 --- a/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl +++ /dev/null @@ -1,40 +0,0 @@ -// VR Stereo Optimizations - Stencil Write Pixel Shader -// -// Reads from the per-pixel mode classification texture. -// Only MODE_MAIN pixels write stencil ref=1 — these are reprojected by ReprojectionCS -// and must be skipped by the geometry pass (NOT_EQUAL stencil test, ref=1). -// -// All other modes (DISOCCLUDED, EDGE, EDGE_NEIGHBOUR, FULL_BLEND) discard so -// geometry renders those pixels normally. ReprojectionCS only fills MODE_MAIN, so -// stencil must not be written for any other mode. -// -// Mode texture is full SBS resolution (same as render target). -// The DSS is configured with StencilFunc=ALWAYS, StencilPassOp=REPLACE, ref=1. -// Pixels that survive (not discarded) get stencil=1 written. - -#include "VRStereoOptimizations/cbuffers.hlsli" - -Texture2D ModeTexture : register(t0); - -struct PS_INPUT -{ - float4 Position: SV_Position; - float2 TexCoord: TEXCOORD0; -}; - -void main(PS_INPUT input) -{ - // Mode texture is full SBS resolution — SV_Position maps directly - // (viewport is Eye 1 half, so SV_Position.x starts at eyeWidth) - int2 modeCoord = int2(input.Position.xy); - - uint mode = ModeTexture[modeCoord]; - - // Only MODE_MAIN pixels are filled by ReprojectionCS and should be stencil-culled. - // EDGE/EDGE_NEIGHBOUR/FULL_BLEND must render normally; DISOCCLUDED is also fully shaded. - if (mode != MODE_MAIN) - discard; - - // Pixel survives: DSS writes stencil ref=1 - // No color output (no RTV bound) -} diff --git a/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl deleted file mode 100644 index 353aa53379..0000000000 --- a/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl +++ /dev/null @@ -1,24 +0,0 @@ -// VR Stereo Optimizations - Stencil Write Vertex Shader -// -// Procedural fullscreen triangle covering Eye 1 (right half of SBS buffer). -// No vertex buffer needed — vertex positions are generated from SV_VertexID. -// The viewport is set to Eye 1 by the C++ code, so we just emit a standard -// fullscreen triangle in clip space. - -struct VS_OUTPUT -{ - float4 Position: SV_Position; - float2 TexCoord: TEXCOORD0; -}; - -VS_OUTPUT main(uint vertexID : SV_VertexID) -{ - VS_OUTPUT output; - - // Fullscreen triangle: 3 vertices covering [-1,1] clip space - float2 uv = float2((vertexID << 1) & 2, vertexID & 2); - output.Position = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); - output.TexCoord = uv; - - return output; -} diff --git a/package/Shaders/VRStereoOptimizations/cbuffers.hlsli b/package/Shaders/VRStereoOptimizations/cbuffers.hlsli deleted file mode 100644 index a7fb7a3961..0000000000 --- a/package/Shaders/VRStereoOptimizations/cbuffers.hlsli +++ /dev/null @@ -1,31 +0,0 @@ -// VR Stereo Optimizations - Shared constant buffer layout -// Must match VRStereoOptParams in VRStereoOptimizations.h exactly - -#ifndef __VR_STEREO_OPT_CBUFFERS_HLSLI__ -#define __VR_STEREO_OPT_CBUFFERS_HLSLI__ - -cbuffer VRStereoOptParams : register(b1) -{ - float2 FrameDim; // Full stereo buffer dimensions (both eyes) - float2 RcpFrameDim; // 1.0 / FrameDim - - uint StereoModeValue; // 0=Off, 1=Enable - float DisocclusionThreshold; // Depth difference threshold for disocclusion detection - float EdgeDepthThreshold; // Relative depth difference threshold for edge detection - uint EdgeWidth; // Half-width of edge detection band in pixels - - float2 QualityJitter; // Sub-pixel jitter offset (Quality mode) - float FoveatedRadius; // Radius of foveal region in UV space - float ForwardOcclusionScale; // Eye 0 depth multiplier for directional disocclusion (0 = disabled) - - float2 FoveatedCenter; // Center of foveal region in UV space - float MinEdgeDistance; - float FullBlendDistance; // Linearized depth below which pixels get MODE_FULL_BLEND (game units) -}; - -#define STEREO_MODE_OFF 0 -#define STEREO_MODE_ENABLE 1 - -#include "VRStereoOptimizations/modes.hlsli" - -#endif diff --git a/package/Shaders/VRStereoOptimizations/modes.hlsli b/package/Shaders/VRStereoOptimizations/modes.hlsli deleted file mode 100644 index b693dedcc3..0000000000 --- a/package/Shaders/VRStereoOptimizations/modes.hlsli +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __VR_STEREO_OPT_MODES_HLSLI__ -#define __VR_STEREO_OPT_MODES_HLSLI__ - -#define MODE_DISOCCLUDED 0 -#define MODE_EDGE 1 -#define MODE_MAIN 2 -#define MODE_EDGE_NEIGHBOUR 3 -#define MODE_FULL_BLEND 4 - -#endif diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index 54998a24e3..899ebe588a 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -71,9 +71,6 @@ struct VS_INPUT float4 Color: COLOR0; # endif # endif -# if defined(VR) - uint InstanceID: SV_INSTANCEID; -# endif // VR }; struct VS_OUTPUT @@ -120,21 +117,13 @@ struct VS_OUTPUT # endif float4 NormalsScale: TEXCOORD8; -# if defined(VR) - float ClipDistance: SV_ClipDistance0; // o11 - float CullDistance: SV_CullDistance0; // p11 -# endif // VR }; # ifdef VSHADER cbuffer PerTechnique : register(b0) { -# if !defined(VR) - float4 QPosAdjust[1] : packoffset(c0); -# else - float4 QPosAdjust[2] : packoffset(c0); -# endif // VR + float4 QPosAdjust : packoffset(c0); }; cbuffer PerMaterial : register(b1) @@ -149,35 +138,22 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - row_major float4x4 World[1] : packoffset(c0); - row_major float4x4 PreviousWorld[1] : packoffset(c4); - row_major float4x4 WorldViewProj[1] : packoffset(c8); + row_major float4x4 World : packoffset(c0); + row_major float4x4 PreviousWorld : packoffset(c4); + row_major float4x4 WorldViewProj : packoffset(c8); float3 ObjectUV : packoffset(c12); float4 CellTexCoordOffset : packoffset(c13); -# else // VR has 25 vs 13 entries - row_major float4x4 World[2] : packoffset(c0); - row_major float4x4 PreviousWorld[2] : packoffset(c8); - row_major float4x4 WorldViewProj[2] : packoffset(c16); - float3 ObjectUV : packoffset(c24); - float4 CellTexCoordOffset : packoffset(c25); -# endif // VR }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout = (VS_OUTPUT)0; - uint eyeIndex = Stereo::GetEyeIndexVS( -# if defined(VR) - input.InstanceID -# endif - ); vsout.NormalsScale = NormalsScale; float4 inputPosition = float4(input.Position.xyz, 1.0); - float4 worldPos = mul(World[eyeIndex], inputPosition); - float4 worldViewPos = mul(WorldViewProj[eyeIndex], inputPosition); + float4 worldPos = mul(World, inputPosition); + float4 worldViewPos = mul(WorldViewProj, inputPosition); float heightMult = min((1.0 / 10000.0) * max(worldViewPos.z - 70000, 0), 1); @@ -187,7 +163,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(STENCIL) vsout.WorldPosition = worldPos; - vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], inputPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld, inputPosition); # else # if !defined(UNIFIED_WATER) @@ -201,7 +177,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(LOD) float4 posAdjust = - ObjectUV.x ? 0.0 : (QPosAdjust[eyeIndex].xyxy + worldPos.xyxy) / NormalsScale.xxyy; + ObjectUV.x ? 0.0 : (QPosAdjust.xyxy + worldPos.xyxy) / NormalsScale.xxyy; vsout.TexCoord1.xyzw = NormalsScroll0 + posAdjust; # else @@ -209,7 +185,7 @@ VS_OUTPUT main(VS_INPUT input) vsout.MPosition.xyzw = inputPosition.xyzw; # endif - float2 posAdjust = worldPos.xy + QPosAdjust[eyeIndex].xy; + float2 posAdjust = worldPos.xy + QPosAdjust.xy; float2 scrollAdjust1 = posAdjust / NormalsScale.xx; float2 scrollAdjust2 = posAdjust / NormalsScale.yy; @@ -296,12 +272,6 @@ VS_OUTPUT main(VS_INPUT input) # endif # endif -# ifdef VR - Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.HPosition, eyeIndex); - vsout.HPosition = VRout.VRPosition; - vsout.ClipDistance.x = VRout.ClipDistance; - vsout.CullDistance.x = VRout.CullDistance; -# endif // VR return vsout; } @@ -351,19 +321,11 @@ Texture2D RawSSRReflectionTex : register(t11); cbuffer PerTechnique : register(b0) { -# if !defined(VR) float4 VPOSOffset : packoffset(c0); // inverse main render target width and height in xy, 0 in zw - float4 PosAdjust[1] : packoffset(c1); // inverse framebuffer range in w + float4 PosAdjust : packoffset(c1); // inverse framebuffer range in w float4 CameraDataWater : packoffset(c2); float4 SunDir : packoffset(c3); float4 SunColor : packoffset(c4); -# else - float4 VPOSOffset : packoffset(c0); // inverse main render target width and height in xy, 0 in zw - float4 PosAdjust[2] : packoffset(c1); // inverse framebuffer range in w - float4 CameraDataWater : packoffset(c3); - float4 SunDir : packoffset(c4); - float4 SunColor : packoffset(c5); -# endif } cbuffer PerMaterial : register(b1) @@ -386,43 +348,13 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { -# if !defined(VR) - float4x4 TextureProj[1] : packoffset(c0); + float4x4 TextureProj : packoffset(c0); float4 ReflectPlane[1] : packoffset(c4); float4 ProjData : packoffset(c5); float4 LightPos[8] : packoffset(c6); float4 LightColor[8] : packoffset(c14); -# else - float4x4 TextureProj[2] : packoffset(c0); - float4 ReflectPlane[2] : packoffset(c8); - float4 ProjData : packoffset(c10); - float4 LightPos[8] : packoffset(c11); - float4 LightColor[8] : packoffset(c19); -# endif //VR } -# if defined(VR) -/** -Calculates the depthMultiplier as used in Water.hlsl - -VR appears to require use of CameraProjInverse and does not use ProjData -@param uv UV coords to convert -@param depth The calculated depth -@param eyeIndex The eyeIndex; 0 is left, 1 is right -@returns depthMultiplier -*/ -float CalculateDepthMultFromUV(float2 uv, float depth, uint eyeIndex = 0) -{ - float4 temp; - temp.xy = (uv * 2 - 1); - temp.z = depth; - temp.w = 1; - temp = mul(FrameBuffer::CameraProjInverse[eyeIndex], temp.xyzw); - temp.xyz /= temp.w; - return length(temp.xyz); -} -# endif // VR - # define SampColorSampler Normals01Sampler # define LinearSampler Normals01Sampler @@ -437,14 +369,14 @@ float CalculateDepthMultFromUV(float2 uv, float depth, uint eyeIndex = 0) # include "Common/ShadowSampling.hlsli" # if defined(SIMPLE) || defined(UNDERWATER) || defined(LOD) || defined(SPECULAR) -float GetWaterFogFade(uint eyeIndex) +float GetWaterFogFade() { # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - return ExponentialHeightFog::GetVanillaFogFade(PosAdjust[eyeIndex].w); + return ExponentialHeightFog::GetVanillaFogFade(PosAdjust.w); } # endif - return PosAdjust[eyeIndex].w; + return PosAdjust.w; } # if defined(FLOWMAP) @@ -575,11 +507,7 @@ float GetFlowmapMipLevel(float2 flowmapUV) float2 textureDims; FlowMapNormalsTex.GetDimensions(textureDims.x, textureDims.y); -# if defined(VR) - textureDims /= 16.0; -# else textureDims /= 8.0; -# endif float2 texCoordsPerSize = flowmapUV * textureDims; float2 dxSize = ddx(texCoordsPerSize); @@ -681,7 +609,7 @@ struct WaterNormalData float4 rippleInfo; // xyz = scaled ripple normal (normalized normal * intensity), w = splash effect intensity }; -WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, uint eyeIndex, float wetnessOcclusion) +WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, float wetnessOcclusion) { WaterNormalData result; result.rippleInfo = float4(0, 0, 0, 0); @@ -828,7 +756,7 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma rippleWPosition.xy += flowOffset; # endif - raindropInfo = WetnessEffects::GetRainDrops(rippleWPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, SharedData::wetnessEffectsSettings.Time, finalNormal, rippleStrengthModifier); + raindropInfo = WetnessEffects::GetRainDrops(rippleWPosition + FrameBuffer::CameraPosAdjust.xyz, SharedData::wetnessEffectsSettings.Time, finalNormal, rippleStrengthModifier); // Calculate ripple and splash color intensities float rippleIntensity = length(raindropInfo.xy) * rippleStrengthModifier; @@ -872,14 +800,8 @@ float3 GetWaterSpecularColor(PS_INPUT input, float3 normal, float3 viewDirection float reflectionAmount = saturate(length(input.WPosition.xyz) / 1024.0); -# if defined(VR) - // Reflection cubemap is incorrect for interiors in VR, ignore it - if (Permutation::PixelShaderDescriptor & Permutation::WaterFlags::Interior || SharedData::HideSky) - reflectionAmount = 0.0; -# else if (SharedData::HideSky) reflectionAmount = 0.0; -# endif reflectionColor = lerp(dynamicCubemap, reflectionColor, reflectionAmount); # endif @@ -902,14 +824,10 @@ float3 GetWaterSpecularColor(PS_INPUT input, float3 normal, float3 viewDirection return reflectionColor; } -float GetScreenDepthWater(float2 screenPosition, uint a_useVR = 0) +float GetScreenDepthWater(float2 screenPosition) { float depth = DepthTex.Load(float3(screenPosition, 0)).x; -# if defined(VR) // VR appears to use hard coded values - return depth * 1.01 + -0.01; -# else return (CameraDataWater.w / (-depth * CameraDataWater.z + CameraDataWater.x)); -# endif } float3 GetLdotN(float3 normal) @@ -943,18 +861,12 @@ struct DiffuseOutput float3 refractedViewDirection; }; -DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection, inout float4 distanceMul, float refractionsDepthFactor, float fresnel, uint eyeIndex, float3 viewPosition, float depth) +DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection, inout float4 distanceMul, float refractionsDepthFactor, float fresnel, float3 viewPosition, float depth) { # if defined(REFRACTIONS) - float4 refractionNormal = mul(transpose(TextureProj[eyeIndex]), float4((VarAmounts.w * refractionsDepthFactor * normal.xy) + input.MPosition.xy, input.MPosition.z, 1)); + float4 refractionNormal = mul(transpose(TextureProj), float4((VarAmounts.w * refractionsDepthFactor * normal.xy) + input.MPosition.xy, input.MPosition.z, 1)); float2 refractionUvRaw = float2(refractionNormal.x, refractionNormal.w - refractionNormal.y) / refractionNormal.ww; - refractionUvRaw = Stereo::ConvertToStereoUV(refractionUvRaw, eyeIndex); // need to convert here for VR due to refractionNormal values - -# if defined(VR) - float2 refractionUvRawNoStereo = Stereo::ConvertFromStereoUV(refractionUvRaw, eyeIndex, 1); -# endif - float2 screenPosition = FrameBuffer::DynamicResolutionParams1.xy * (FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy); float2 refractionScreenPosition = FrameBuffer::DynamicResolutionParams1.xy * (refractionUvRaw / VPOSOffset.xy); @@ -963,27 +875,19 @@ DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDir # if defined(DEPTH) && !defined(VERTEX_ALPHA_DEPTH) float refractionDepth = GetScreenDepthWater(refractionScreenPosition); depth = refractionDepth; -# if !defined(VR) float refractionDepthMul = length(float3((((VPOSOffset.zw + refractionUvRaw) * 2 - 1)) * refractionDepth / ProjData.xy, refractionDepth)); -# else - float refractionDepthMul = CalculateDepthMultFromUV(refractionUvRawNoStereo, refractionDepth, eyeIndex); -# endif //VR float3 refractionDepthAdjustedViewDirection = -viewDirection * refractionDepthMul; - float refractionViewSurfaceAngle = dot(refractionDepthAdjustedViewDirection, ReflectPlane[eyeIndex].xyz); + float refractionViewSurfaceAngle = dot(refractionDepthAdjustedViewDirection, ReflectPlane[0].xyz); - float refractionPlaneMul = (1 - ReflectPlane[eyeIndex].w / refractionViewSurfaceAngle); + float refractionPlaneMul = (1 - ReflectPlane[0].w / refractionViewSurfaceAngle); if (refractionPlaneMul < 0.0) { - refractionUvRaw = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; // This value is already stereo converted for VR + refractionUvRaw = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; } else { distanceMul = saturate(refractionPlaneMul * float4(length(refractionDepthAdjustedViewDirection).xx, abs(refractionViewSurfaceAngle).xx) / FogParam.z); -# if defined(VR) - refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4((refractionUvRawNoStereo * 2 - 1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); -# else - refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4((refractionUvRaw * 2 - 1) * float2(1, -1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); -# endif + refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse, float4((refractionUvRaw * 2 - 1) * float2(1, -1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); refractionWorldPosition.xyz /= refractionWorldPosition.w; } # endif @@ -1016,7 +920,7 @@ DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDir # endif } -float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition, uint eyeIndex) +float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition) { # if defined(UNDERWATER) return 0.0.xxx; @@ -1031,7 +935,7 @@ float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition, ui float3 sunColor = Color::DirectionalLight((SunColor.xyz * SunDir.w) / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * (1.0 - exp(-DeepColor.w)) * llDirLightMult; # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - sunColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); + sunColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); } # endif return reflectionMul * sunColor; @@ -1055,7 +959,6 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; - uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); float2 screenPosition = FrameBuffer::DynamicResolutionParams1.xy * (FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy); # if defined(SIMPLE) || defined(UNDERWATER) || defined(LOD) || defined(SPECULAR) @@ -1083,15 +986,11 @@ PS_OUTPUT main(PS_INPUT input) depth = GetScreenDepthWater(screenPosition); float2 depthOffset = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; -# if !defined(VR) float depthMul = length(float3((depthOffset * 2 - 1) * depth / ProjData.xy, depth)); -# else - float depthMul = CalculateDepthMultFromUV(Stereo::ConvertFromStereoUV(depthOffset, eyeIndex, 1), depth, eyeIndex); -# endif //VR float3 depthAdjustedViewDirection = -viewDirection * depthMul; - float viewSurfaceAngle = dot(depthAdjustedViewDirection, ReflectPlane[eyeIndex].xyz); + float viewSurfaceAngle = dot(depthAdjustedViewDirection, ReflectPlane[0].xyz); - float planeMul = (1 - ReflectPlane[eyeIndex].w / viewSurfaceAngle); + float planeMul = (1 - ReflectPlane[0].w / viewSurfaceAngle); distanceMul = saturate( planeMul * float4(length(depthAdjustedViewDirection).xx, abs(viewSurfaceAngle).xx) / FogParam.z); @@ -1107,18 +1006,14 @@ PS_OUTPUT main(PS_INPUT input) # else float4 depthControl = DepthControl * (distanceMul - 1) + 1; # endif - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); + float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition); const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); # if defined(SKYLIGHTING) float wetnessOcclusion = 1.0; -# if defined(VR) - float3 positionMSSkylight = input.WPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; -# else float3 positionMSSkylight = input.WPosition.xyz; -# endif sh2 skylightingSH = Skylighting::SampleNoBias(positionMSSkylight); float skylighting = SphericalHarmonics::Unproject(skylightingSH, float3(0, 0, 1)); @@ -1129,9 +1024,9 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(SKYLIGHTING) - WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, eyeIndex, wetnessOcclusion); + WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, wetnessOcclusion); # else - WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, eyeIndex, inWorld); + WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, inWorld); # endif float3 normal = waterData.normal; @@ -1148,7 +1043,7 @@ PS_OUTPUT main(PS_INPUT input) [unroll] for (int lightIndex = 0; lightIndex < NUM_SPECULAR_LIGHTS; ++lightIndex) { - float3 lightVector = LightPos[lightIndex].xyz - (PosAdjust[eyeIndex].xyz + input.WPosition.xyz); + float3 lightVector = LightPos[lightIndex].xyz - (PosAdjust.xyz + input.WPosition.xyz); float3 lightDirection = normalize(normalize(lightVector) - viewDirection); float lightFade = saturate(length(lightVector) / LightPos[lightIndex].w); float lightColorMul = (1 - lightFade * lightFade); @@ -1175,10 +1070,10 @@ PS_OUTPUT main(PS_INPUT input) float3 specularColor = GetWaterSpecularColor(input, normal, viewDirection, distanceFactor, 1.0); # endif - DiffuseOutput diffuseOutput = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, eyeIndex, viewPosition, depth); + DiffuseOutput diffuseOutput = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, viewPosition, depth); float surfaceShadow; - float dirShadow = ShadowSampling::Get3DFilteredShadow(input.WPosition.xyz, diffuseOutput.refractedViewDirection, input.HPosition.xy, eyeIndex, surfaceShadow); + float dirShadow = ShadowSampling::Get3DFilteredShadow(input.WPosition.xyz, diffuseOutput.refractedViewDirection, input.HPosition.xy, surfaceShadow); float3 dirColor; float3 ambientColor; @@ -1219,7 +1114,7 @@ PS_OUTPUT main(PS_INPUT input) continue; } - float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WPosition.xyz; + float3 lightDirection = light.positionWS.xyz - input.WPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -1255,7 +1150,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # else - float3 sunColor = GetSunColor(normal, viewDirection, input.WPosition.xyz, eyeIndex) * surfaceShadow; + float3 sunColor = GetSunColor(normal, viewDirection, input.WPosition.xyz) * surfaceShadow; # if defined(VC) float specularFraction = lerp(1, fresnel * diffuseOutput.refractionMul, distanceBlendFactor); @@ -1278,23 +1173,23 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { fogColor = exponentialHeightFog.xyz; - fogColor *= GetWaterFogFade(eyeIndex); + fogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, fogColor, exponentialHeightFog.w); } else { - fogColor *= GetWaterFogFade(eyeIndex); + fogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); - float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(eyeIndex); + float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, expFogColor, exponentialHeightFog.w); } } else { - fogColor *= GetWaterFogFade(eyeIndex); + fogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); } # else - fogColor *= GetWaterFogFade(eyeIndex); + fogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); # endif @@ -1329,23 +1224,23 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, preFogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, preFogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { preFogColor = exponentialHeightFog.xyz; - preFogColor *= GetWaterFogFade(eyeIndex); + preFogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, preFogColor, exponentialHeightFog.w); } else { - preFogColor *= GetWaterFogFade(eyeIndex); + preFogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); - float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(eyeIndex); + float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, expFogColor, exponentialHeightFog.w); } } else { - preFogColor *= GetWaterFogFade(eyeIndex); + preFogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); } # else - preFogColor *= GetWaterFogFade(eyeIndex); + preFogColor *= GetWaterFogFade(); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); # endif diff --git a/src/CSEditor/Weather/PrecipitationWidget.cpp b/src/CSEditor/Weather/PrecipitationWidget.cpp index 8efa1d915a..17cca25148 100644 --- a/src/CSEditor/Weather/PrecipitationWidget.cpp +++ b/src/CSEditor/Weather/PrecipitationWidget.cpp @@ -235,7 +235,7 @@ void PrecipitationWidget::LoadFromGameSettings() settings.particleType = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleType).i; settings.boxSize = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kBoxSize).f; settings.particleDensity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; - GET_INSTANCE_MEMBER(particleTexture, precipitation) + auto& particleTexture = precipitation->GetRuntimeData().particleTexture; settings.particleTexture = particleTexture.textureName.c_str(); } @@ -276,7 +276,7 @@ void PrecipitationWidget::ApplyChanges() precipitation->GetSettingRef(DataID::kParticleType).i = settings.particleType; precipitation->GetSettingRef(DataID::kBoxSize).f = settings.boxSize; precipitation->GetSettingRef(DataID::kParticleDensity).f = settings.particleDensity; - GET_INSTANCE_MEMBER(particleTexture, precipitation) + auto& particleTexture = precipitation->GetRuntimeData().particleTexture; particleTexture.textureName = settings.particleTexture.c_str(); ApplyLiveParticleTexture(settings.particleTexture); Widget::ForceCurrentWeatherReinit(); diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 9c9a747960..aa680f2aa3 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -13,7 +13,6 @@ #include "Features/SubsurfaceScattering.h" #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" -#include "Features/VR.h" #include "Features/CSEditor.h" #include "Hooks.h" @@ -228,9 +227,9 @@ void Deferred::StartDeferred() globals::state->UpdateSharedData(true, false); auto shadowState = globals::game::shadowState; - GET_INSTANCE_MEMBER(renderTargets, shadowState) - GET_INSTANCE_MEMBER(setRenderTargetMode, shadowState) - GET_INSTANCE_MEMBER(stateUpdateFlags, shadowState) + auto& renderTargets = shadowState->GetRuntimeData().renderTargets; + auto& setRenderTargetMode = shadowState->GetRuntimeData().setRenderTargetMode; + auto& stateUpdateFlags = shadowState->GetRuntimeData().stateUpdateFlags; // Backup original render targets for (uint i = 0; i < 4; i++) { @@ -260,35 +259,13 @@ void Deferred::StartDeferred() { auto context = globals::d3d::context; - // Clear POM offset texture to -1.0 sentinel so pixels the Lighting PS never touches read "no POM" - if (globals::features::vr.stereoOpt.loaded) - globals::features::vr.stereoOpt.ClearPomOffsetTexture(); - ID3D11Buffer* buffers[1] = { *globals::game::perFrame.get() }; - - ID3D11Buffer* vrBuffer = nullptr; - - if (REL::Module::IsVR()) { - static REL::Relocation VRValues{ REL::Offset(0x3180688) }; - vrBuffer = *VRValues.get(); - } - if (vrBuffer) { - context->CSSetConstantBuffers(12, 1, buffers); - context->CSSetConstantBuffers(13, 1, &vrBuffer); - } else { - context->CSSetConstantBuffers(12, 1, buffers); - } + context->CSSetConstantBuffers(12, 1, buffers); } PrepassPasses(); OverrideBlendStates(); - - // VR: Classify Eye 1 pixels and write hardware stencil marks before geometry rendering. - // Only enable stencil culling when overwrite reprojection is available for this frame. - if (globals::game::isVR && globals::features::vr.IsStereoOptimizationCullingReady()) { - globals::features::vr.stereoOpt.DispatchStencil(); - } } void Deferred::DeferredPasses() @@ -301,18 +278,7 @@ void Deferred::DeferredPasses() { ID3D11Buffer* buffers[1] = { *globals::game::perFrame }; - ID3D11Buffer* vrBuffer = nullptr; - - if (REL::Module::IsVR()) { - static REL::Relocation VRValues{ REL::Offset(0x3180688) }; - vrBuffer = *VRValues.get(); - } - if (vrBuffer) { - context->CSSetConstantBuffers(12, 1, buffers); - context->CSSetConstantBuffers(13, 1, &vrBuffer); - } else { - context->CSSetConstantBuffers(12, 1, buffers); - } + context->CSSetConstantBuffers(12, 1, buffers); } auto specular = renderer->GetRuntimeData().renderTargets[SPECULAR]; @@ -359,7 +325,7 @@ void Deferred::DeferredPasses() albedo.SRV, // t1 AlbedoTexture normalRoughness.SRV, // t2 NormalRoughnessTexture masks.SRV, // t3 MasksTexture - dynamicCubemaps.loaded || REL::Module::IsVR() ? Util::GetCurrentSceneDepthSRV(false) : nullptr, // t4 DepthTexture (24/32-bit; HLSL type baked at compile via TERRAIN_BLENDING) + dynamicCubemaps.loaded ? Util::GetCurrentSceneDepthSRV(false) : nullptr, // t4 DepthTexture (24/32-bit; HLSL type baked at compile via TERRAIN_BLENDING) dynamicCubemaps.loaded ? reflectance.SRV : nullptr, // t5 ReflectanceTexture dynamicCubemaps.loaded ? dynamicCubemaps.envTexture->srv.get() : nullptr, // t6 EnvTexture dynamicCubemaps.loaded ? dynamicCubemaps.envReflectionsTexture->srv.get() : nullptr, // t7 EnvReflectionsTexture @@ -378,14 +344,6 @@ void Deferred::DeferredPasses() context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); - // Bind VRStereoOptimizations mode texture for Eye 1 skip. - // Bind null when disabled so stale mode data doesn't cause incorrect early-exits - // in DeferredCompositeCS (null SRV reads return 0 = MODE_DISOCCLUDED, all pixels composite normally). - auto& vrStereoOpt = globals::features::vr.stereoOpt; - bool stereoCullingReady = globals::features::vr.IsStereoOptimizationCullingReady(); - ID3D11ShaderResourceView* modeSRV = stereoCullingReady ? vrStereoOpt.GetModeTextureSRV() : nullptr; - context->CSSetShaderResources(16, 1, &modeSRV); - ID3D11UnorderedAccessView* uavs[3]{ main.UAV, normals.UAV, motionVectors.UAV }; context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); @@ -398,27 +356,6 @@ void Deferred::DeferredPasses() context->Dispatch(dispatchCount.x, dispatchCount.y, 1); globals::profiler->EndPass(); } - - // Unbind mode texture SRV - ID3D11ShaderResourceView* nullSRV = nullptr; - context->CSSetShaderResources(16, 1, &nullSRV); - } - - // VR: Deactivate stencil culling now that geometry rendering is complete. - // Must happen before StereoBlend so the blend pass itself isn't stencil-blocked. - if (globals::game::isVR) { - auto& stereoOpt = globals::features::vr.stereoOpt; - if (stereoOpt.IsStencilActive()) { - stereoOpt.DeactivateStencil(); - } - } - - // VR: Stereo reprojection fills Eye 1 holes here (after DeferredComposite, before SSR/water/sky) - // so that ISReflectionsRayTracing sees valid pixels in both eyes. - if (globals::game::isVR) { - globals::profiler->BeginPass("VR::StereoBlend"); - globals::features::vr.DrawStereoBlend(); - globals::profiler->EndPass(); } // Clear @@ -450,8 +387,8 @@ void Deferred::EndDeferred() return; auto shadowState = globals::game::shadowState; - GET_INSTANCE_MEMBER(renderTargets, shadowState) - GET_INSTANCE_MEMBER(stateUpdateFlags, shadowState) + auto& renderTargets = shadowState->GetRuntimeData().renderTargets; + auto& stateUpdateFlags = shadowState->GetRuntimeData().stateUpdateFlags; // Do not render to our targets past this point for (uint i = 0; i < 4; i++) { @@ -593,10 +530,7 @@ void Deferred::CopyShadowLightData() dd.EndSplitDistances = { dirData.endSplitDistances[0], dirData.endSplitDistances[1] }; dd.StartSplitDistances = { dirData.startSplitDistances[0], dirData.startSplitDistances[1] }; - if (globals::game::isVR) - SetShadowCascadeParameters(sunShadowLight->GetVRRuntimeData(), dd); - else - SetShadowCascadeParameters(sunShadowLight->GetRuntimeData(), dd); + SetShadowCascadeParameters(sunShadowLight->GetRuntimeData(), dd); D3D11_MAPPED_SUBRESOURCE mapped{}; DX::ThrowIfFailed(context->Map(directionalShadowLights->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); @@ -638,12 +572,6 @@ ID3D11ComputeShader* Deferred::GetComputeMainComposite() if (globals::features::ibl.loaded) defines.push_back({ "IBL", nullptr }); - if (REL::Module::IsVR()) - defines.push_back({ "FRAMEBUFFER", nullptr }); - - if (REL::Module::IsVR()) - defines.push_back({ "VR_STEREO_OPT", nullptr }); - // TERRAIN_BLENDING flips DepthTexture's HLSL type from `Texture2D` // (R24_UNORM_X8_TYPELESS game depth) to `Texture2D` (R32_FLOAT blendedDepth). if (globals::features::terrainBlending.loaded) @@ -671,12 +599,6 @@ ID3D11ComputeShader* Deferred::GetComputeMainCompositeInterior() if (globals::features::ibl.loaded) defines.push_back({ "IBL", nullptr }); - if (REL::Module::IsVR()) - defines.push_back({ "FRAMEBUFFER", nullptr }); - - if (REL::Module::IsVR()) - defines.push_back({ "VR_STEREO_OPT", nullptr }); - // TERRAIN_BLENDING flips DepthTexture's HLSL type from `Texture2D` // (R24_UNORM_X8_TYPELESS game depth) to `Texture2D` (R32_FLOAT blendedDepth). if (globals::features::terrainBlending.loaded) diff --git a/src/Deferred.h b/src/Deferred.h index c95f28ed2a..b18002d4bd 100644 --- a/src/Deferred.h +++ b/src/Deferred.h @@ -122,14 +122,13 @@ class Deferred { stl::write_vfunc<0x35, BSCubeMapCamera_RenderCubemap>(RE::VTABLE_BSCubeMapCamera[0]); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x2EC, 0x2EC, 0x248)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x2EC, 0x2EC)); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x831, 0x841, 0x791)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x831, 0x841)); stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x8E, 0x84)); - stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x319, 0x308, 0x321)); + stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x319, 0x308)); - if (!REL::Module::IsVR()) - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x944, 0x954)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x944, 0x954)); stl::detour_thunk(REL::RelocationID(75570, 77371)); diff --git a/src/EngineFixes/ShadowmapCascadeCullingFix.cpp b/src/EngineFixes/ShadowmapCascadeCullingFix.cpp index aed49dde1d..1b537a3ac7 100644 --- a/src/EngineFixes/ShadowmapCascadeCullingFix.cpp +++ b/src/EngineFixes/ShadowmapCascadeCullingFix.cpp @@ -4,7 +4,7 @@ void ShadowmapCascadeCullingFix::Install() { gfSplitOverlap = reinterpret_cast(REL::RelocationID(513805, 391863).address()); - stl::write_thunk_call(REL::RelocationID(101499, 108496).address() + REL::Relocate(0x1B12, 0x1C02, 0x1C82)); + stl::write_thunk_call(REL::RelocationID(101499, 108496).address() + REL::Relocate(0x1B12, 0x1C02)); } void ShadowmapCascadeCullingFix::BSShadowDirectionalLight_SetFrameCamera_BuildCascadeCameraCullingPlanes::thunk(RE::BSShadowDirectionalLight* dirLight, RE::NiFrustumPlanes& outPlanes, FrustumSplit& frustumSplit, uint32_t splitCornerIndices[8], uint32_t numSplitCornerIndices, RE::NiPoint3& lightDir, RE::NiPoint3& cameraPos, uint32_t cornerOffsetIndex) diff --git a/src/EngineFixes/ShadowmapCascadeRasterizerFix.cpp b/src/EngineFixes/ShadowmapCascadeRasterizerFix.cpp index 2b6881b659..d5f334360f 100644 --- a/src/EngineFixes/ShadowmapCascadeRasterizerFix.cpp +++ b/src/EngineFixes/ShadowmapCascadeRasterizerFix.cpp @@ -3,7 +3,7 @@ void ShadowmapRasterizerFix::Install() { // This function is called once per cascade to begin the updating and rendering process - stl::write_thunk_call(REL::RelocationID(101495, 108489).address() + REL::Relocate(0xC6, 0xC6, 0xF6)); + stl::write_thunk_call(REL::RelocationID(101495, 108489).address() + REL::Relocate(0xC6, 0xC6)); gRasterStates = reinterpret_cast(REL::RelocationID(524748, 411363).address()); diff --git a/src/EngineFixes/ShadowmapCascadeRasterizerFix.h b/src/EngineFixes/ShadowmapCascadeRasterizerFix.h index 82c4997341..3d0cfa5a5b 100644 --- a/src/EngineFixes/ShadowmapCascadeRasterizerFix.h +++ b/src/EngineFixes/ShadowmapCascadeRasterizerFix.h @@ -54,6 +54,6 @@ struct ShadowmapRasterizerFix : EngineFix "Controls the number of shadow map cascades used for directional lighting. " "Higher values provide better shadow quality but use more GPU resources. " "Maximum of 3 cascades supported. ", - REL::Relocate(0, 0, 0x1ed6350), 2, 1, 3 } }, + static_cast(0), 2, 1, 3 } }, }; }; diff --git a/src/Feature.cpp b/src/Feature.cpp index 6fb92d66de..48511574fa 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -33,7 +33,6 @@ #include "Features/TerrainVariation.h" #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" -#include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -251,34 +250,7 @@ const std::vector& Feature::GetFeatureList() &globals::features::skin }; - if (REL::Module::IsVR()) { - // Helper function to build VR feature list - static auto BuildVRList = []() -> std::vector { - auto v = features; - v.push_back(&globals::features::vr); - - // In developer mode, keep all features for testing - // In production mode, filter to VR-compatible only - if (!globals::state->IsDeveloperMode()) { - std::erase_if(v, [](Feature* a) { return !a->SupportsVR(); }); - } - return v; - }; - - // Cache the VR feature list but invalidate when developer mode changes - static std::vector featuresVR; - static bool cachedDevMode = false; - - bool currentDevMode = globals::state->IsDeveloperMode(); - if (featuresVR.empty() || currentDevMode != cachedDevMode) { - featuresVR = BuildVRList(); - cachedDevMode = currentDevMode; - } - - return featuresVR; - } else { - return features; - } + return features; } Feature* Feature::FindFeatureByShortName(const std::string& shortName) diff --git a/src/Feature.h b/src/Feature.h index a82064acaf..92c61ddc2f 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -49,12 +49,6 @@ struct Feature public: virtual bool HasShaderDefine(RE::BSShader::Type) { return false; } - /** - * Whether the feature supports VR. - * - * \return true if VR supported; else false - */ - virtual bool SupportsVR() { return false; } /** * Whether the feature is a CORE feature diff --git a/src/FeatureConstraints.h b/src/FeatureConstraints.h index 8437025f5b..23a0a3cf6b 100644 --- a/src/FeatureConstraints.h +++ b/src/FeatureConstraints.h @@ -11,7 +11,7 @@ namespace FeatureConstraints */ struct SettingId { - std::string featureShortName; // e.g., "VR" + std::string featureShortName; std::string settingPath; // e.g., "EnableDepthBufferCullingExterior" bool operator==(const SettingId& other) const diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index 3e8ad91a53..9db14ed73d 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -846,12 +846,6 @@ namespace FeatureIssues continue; } - // Skip VR feature when not in VR mode (it's a core feature) - if (featureName == "VR" && !REL::Module::IsVR()) { - logger::info("Ignoring VR.ini in non-VR mode"); - continue; - } - // This is an orphaned INI file - check if it's a known obsolete feature if (IsObsoleteFeature(featureName)) { // Read version from INI file diff --git a/src/FeatureIssues.h b/src/FeatureIssues.h index ce6103cdd5..80ad8bab3b 100644 --- a/src/FeatureIssues.h +++ b/src/FeatureIssues.h @@ -165,7 +165,7 @@ namespace FeatureIssues * * This function scans the Data/Shaders/Features/ directory for INI files that * correspond to features not currently in the active feature list (e.g., obsolete - * features, VR features in non-VR mode, unknown features). It identifies whether + * features, unknown features). It identifies whether * these orphaned INI files are known obsolete features or completely unknown features * and adds them to the feature issues tracking system. * diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index 8fd88bccac..8c96c254ab 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -493,7 +493,7 @@ void CSEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) } auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; ImGui::BulletText(T(TKEY("particle_density"), "Particle Density: %.3f"), particleDensity); - GET_INSTANCE_MEMBER(particleTexture, weather->precipitationData) + auto& particleTexture = weather->precipitationData->GetRuntimeData().particleTexture; if (!particleTexture.textureName.empty()) { ImGui::BulletText(T(TKEY("particle_texture"), "Particle Texture: %s"), particleTexture.textureName.c_str()); } else { diff --git a/src/Features/CSEditor.h b/src/Features/CSEditor.h index 554731c393..0841dec4f1 100644 --- a/src/Features/CSEditor.h +++ b/src/Features/CSEditor.h @@ -20,7 +20,6 @@ struct CSEditor : OverlayFeature virtual inline std::string GetShortName() override { return "CSEditor"; } virtual inline std::string_view GetShaderDefineName() override { return "CS_EDITOR"; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kUtility; } - virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } virtual bool IsInMenu() const override { return true; } diff --git a/src/Features/CloudShadows.cpp b/src/Features/CloudShadows.cpp index fe958a0ce1..2a3d5392d8 100644 --- a/src/Features/CloudShadows.cpp +++ b/src/Features/CloudShadows.cpp @@ -99,7 +99,7 @@ void CloudShadows::ModifySky(RE::BSRenderPass* Pass) { auto shadowState = globals::game::shadowState; - GET_INSTANCE_MEMBER(cubeMapRenderTarget, shadowState); + auto& cubeMapRenderTarget = shadowState->GetRuntimeData().cubeMapRenderTarget; if (cubeMapRenderTarget != RE::RENDER_TARGETS_CUBEMAP::kREFLECTIONS) return; diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index f25123a035..0e75f6cb03 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -74,5 +74,4 @@ struct CloudShadows : Feature logger::info("[Cloud Shadows] Installed hooks"); } }; - virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 5721ee8f35..69dd37837e 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -33,13 +33,6 @@ void DynamicCubemaps::DrawSettings() recompileFlag |= ImGui::Checkbox(T(TKEY("enable_ssr"), "Enable Screen Space Reflections"), reinterpret_cast(&settings.EnabledSSR)); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", T(TKEY("enable_ssr_tooltip"), "Enable Screen Space Reflections on Water")); - if (REL::Module::IsVR() && !enabledAtBoot) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); - ImGui::Text("%s", T(TKEY("vr_restart_required"), - "A restart is required to enable in VR. " - "Save Settings after enabling and restart the game.")); - ImGui::PopStyleColor(); - } } ImGui::TreePop(); } @@ -122,69 +115,32 @@ void DynamicCubemaps::DrawSettings() } ImGui::TreePop(); } - if (REL::Module::IsVR()) { - if (ImGui::TreeNodeEx(T(TKEY("advanced_vr_settings"), "Advanced VR Settings"), ImGuiTreeNodeFlags_DefaultOpen)) { - Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR"); - Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR"); - ImGui::TreePop(); - } - } } void DynamicCubemaps::LoadSettings(json& o_json) { settings = o_json; - if (REL::Module::IsVR()) { - Util::LoadGameSettings(iniVRCubeMapSettings); - } recompileFlag = true; } void DynamicCubemaps::SaveSettings(json& o_json) { o_json = settings; - if (REL::Module::IsVR()) { - Util::SaveGameSettings(iniVRCubeMapSettings); - } } void DynamicCubemaps::RestoreDefaultSettings() { settings = {}; - if (REL::Module::IsVR()) { - Util::ResetGameSettingsToDefaults(iniVRCubeMapSettings); - Util::ResetGameSettingsToDefaults(hiddenVRCubeMapSettings); - } recompileFlag = true; } void DynamicCubemaps::DataLoaded() { - if (REL::Module::IsVR()) { - // enable cubemap settings in VR - Util::EnableBooleanSettings(iniVRCubeMapSettings, GetName()); - Util::EnableBooleanSettings(hiddenVRCubeMapSettings, GetName()); - } MenuOpenCloseEventHandler::Register(); } void DynamicCubemaps::PostPostLoad() { - if (REL::Module::IsVR() && settings.EnabledSSR) { - std::map earlyhiddenVRCubeMapSettings{ - { "bScreenSpaceReflectionEnabled:Display", 0x1ED5BC0 }, - }; - for (const auto& settingPair : earlyhiddenVRCubeMapSettings) { - const auto& settingName = settingPair.first; - const auto address = REL::Offset{ settingPair.second }.address(); - bool* setting = reinterpret_cast(address); - if (!*setting) { - logger::info("[PostPostLoad] Changing {} from {} to {} to support Dynamic Cubemaps", settingName, *setting, true); - *setting = true; - } - } - enabledAtBoot = true; - } } RE::BSEventNotifyControl MenuOpenCloseEventHandler::ProcessEvent(const RE::MenuOpenCloseEvent* a_event, RE::BSTEventSource*) @@ -364,7 +320,7 @@ void DynamicCubemaps::UpdateCubemapCapture(bool a_reflections) static float3 cameraPreviousPosAdjust[2] = { { 0, 0, 0 }, { 0, 0, 0 } }; updateData.CameraPreviousPosAdjust = cameraPreviousPosAdjust[index]; - auto eyePosition = Util::GetEyePosition(0); + auto eyePosition = Util::GetEyePosition(); cameraPreviousPosAdjust[index] = { eyePosition.x, eyePosition.y, eyePosition.z }; diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index e26615f6cf..bf94b7db0f 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -119,7 +119,6 @@ struct DynamicCubemaps : Feature }; Settings settings; - bool enabledAtBoot = false; void UpdateCubemap(); void PostDeferred(); @@ -135,8 +134,7 @@ struct DynamicCubemaps : Feature { T("feature.dynamic_cubemaps.key_feature_1", "Real-time environment capture for realistic reflections"), T("feature.dynamic_cubemaps.key_feature_2", "Dynamic cube map generation based on camera position"), T("feature.dynamic_cubemaps.key_feature_3", "Enhanced water reflections with environmental details"), - T("feature.dynamic_cubemaps.key_feature_4", "Support for both standard and VR rendering modes"), - T("feature.dynamic_cubemaps.key_feature_5", "Optimized cubemap inference and irradiance calculation") } }; + T("feature.dynamic_cubemaps.key_feature_4", "Optimized cubemap inference and irradiance calculation") } }; }; virtual std::vector> GetShaderDefineOptions() override; @@ -153,20 +151,6 @@ struct DynamicCubemaps : Feature virtual void DataLoaded() override; virtual void PostPostLoad() override; - std::map iniVRCubeMapSettings{ - { "bAutoWaterSilhouetteReflections:Water", { "Auto Water Silhouette Reflections", "Automatically reflects silhouettes on water surfaces.", 0, true, false, true } }, - { "bForceHighDetailReflections:Water", { "Force High Detail Reflections", "Forces the use of high-detail reflections on water surfaces.", 0, true, false, true } } - }; - - std::map hiddenVRCubeMapSettings{ - { "bReflectExplosions:Water", { "Reflect Explosions", "Enables reflection of explosions on water surfaces.", 0x1eaa000, true, false, true } }, - { "bReflectLODLand:Water", { "Reflect LOD Land", "Enables reflection of low-detail (LOD) terrain on water surfaces.", 0x1eaa060, true, false, true } }, - { "bReflectLODObjects:Water", { "Reflect LOD Objects", "Enables reflection of low-detail (LOD) objects on water surfaces.", 0x1eaa078, true, false, true } }, - { "bReflectLODTrees:Water", { "Reflect LOD Trees", "Enables reflection of low-detail (LOD) trees on water surfaces.", 0x1eaa090, true, false, true } }, - { "bReflectSky:Water", { "Reflect Sky", "Enables reflection of the sky on water surfaces.", 0x1eaa0a8, true, false, true } }, - { "bUseWaterRefractions:Water", { "Use Water Refractions", "Enables refractions for water surfaces, affecting how light bends through water.", 0x1eaa0c0, true, false, true } } - }; - virtual void ClearShaderCache() override; ID3D11ComputeShader* GetComputeShaderUpdate(); ID3D11ComputeShader* GetComputeShaderUpdateReflections(); @@ -188,6 +172,5 @@ struct DynamicCubemaps : Feature ID3D11ComputeShader* GetComputeShaderBC6HEncode(); - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/ExponentialHeightFog.cpp b/src/Features/ExponentialHeightFog.cpp index 4eb4c1cb9d..64108cdef1 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -211,7 +211,8 @@ void ExponentialHeightFog::EnsureVolumetricResources() { uint32_t pixelSize = std::clamp(settings.volumetricGridPixelSize, 4u, 64u); const uint32_t gridZ = std::clamp(settings.volumetricGridSizeZ, 16u, 160u); - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + float2 screenSz{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto renderSize = Util::ConvertToDynamic(screenSz); auto getGridSize = [&renderSize, gridZ](uint32_t a_pixelSize) { return DirectX::XMUINT4{ @@ -440,13 +441,7 @@ void ExponentialHeightFog::Prepass() 0.0f }; - const uint32_t eyeCount = globals::game::isVR ? 2u : 1u; - for (uint32_t eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { - cb.clipToWorld[eyeIndex] = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Invert(); - } - if (eyeCount == 1u) { - cb.clipToWorld[1] = cb.clipToWorld[0]; - } + cb.clipToWorld = globals::game::frameBufferCached.GetCameraViewProjUnjittered().Invert(); for (uint32_t i = 0; i < std::size(cb.frameJitterOffsets); i++) { const uint32_t temporalFrame = (globals::state->frameCount - i) & 1023u; diff --git a/src/Features/ExponentialHeightFog.h b/src/Features/ExponentialHeightFog.h index b386df78e5..93b032b591 100644 --- a/src/Features/ExponentialHeightFog.h +++ b/src/Features/ExponentialHeightFog.h @@ -8,7 +8,6 @@ struct ExponentialHeightFog : Feature static constexpr std::string_view MOD_ID = "180146"; public: - virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Exponential Height Fog"; } virtual std::string GetDisplayName() override { return T("feature.exponential_height_fog.name", "Exponential Height Fog"); } virtual inline std::string GetShortName() override { return "ExponentialHeightFog"; } @@ -84,7 +83,7 @@ struct ExponentialHeightFog : Feature DirectX::XMUINT4 gridSizeAndFlags = {}; float4 invGridSizeAndNearFade = {}; float4 gridZParams = {}; - float4x4 clipToWorld[2] = {}; + float4x4 clipToWorld = {}; float4 frameJitterOffsets[16] = {}; float4 historyParameters = {}; float4 jitterParameters = {}; // x = LightScatteringSampleJitterMultiplier, y = StateFrameIndexMod8, zw = unused diff --git a/src/Features/ExtendedMaterials.h b/src/Features/ExtendedMaterials.h index 0f3bd19a4d..7254daf12e 100644 --- a/src/Features/ExtendedMaterials.h +++ b/src/Features/ExtendedMaterials.h @@ -49,6 +49,5 @@ struct ExtendedMaterials : Feature virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/ExtendedTranslucency.h b/src/Features/ExtendedTranslucency.h index fd40a8f0cb..61706c0379 100644 --- a/src/Features/ExtendedTranslucency.h +++ b/src/Features/ExtendedTranslucency.h @@ -25,7 +25,6 @@ struct ExtendedTranslucency final : Feature virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; static void BSLightingShader_SetupGeometry(RE::BSRenderPass* pass); diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index 8f98fea1cb..eb288c6627 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -40,7 +40,7 @@ void GrassCollision::QueueCollisions() return; eastl::vector actorCandidates{}; - RE::NiPoint3 cameraPosition = Util::GetEyePosition(0); + RE::NiPoint3 cameraPosition = Util::GetEyePosition(); auto addActorCandidate = [&](RE::ActorHandle a_handle) { auto actor = a_handle.get(); @@ -160,7 +160,7 @@ void GrassCollision::Update() static float2 prevCellID = { 0, 0 }; - auto eyePosNI = Util::GetEyePosition(0); + auto eyePosNI = Util::GetEyePosition(); static auto prevEyePosNI = eyePosNI; auto eyePos = float2{ eyePosNI.x, eyePosNI.y }; @@ -371,18 +371,7 @@ void GrassCollision::UpdateCollisionTexture() { ID3D11Buffer* buffers[1] = { *globals::game::perFrame }; - ID3D11Buffer* vrBuffer = nullptr; - - if (REL::Module::IsVR()) { - static REL::Relocation VRValues{ REL::Offset(0x3180688) }; - vrBuffer = *VRValues.get(); - } - if (vrBuffer) { - context->CSSetConstantBuffers(12, 1, buffers); - context->CSSetConstantBuffers(13, 1, &vrBuffer); - } else { - context->CSSetConstantBuffers(12, 1, buffers); - } + context->CSSetConstantBuffers(12, 1, buffers); } { diff --git a/src/Features/GrassCollision.h b/src/Features/GrassCollision.h index da25c5e150..e133077b71 100644 --- a/src/Features/GrassCollision.h +++ b/src/Features/GrassCollision.h @@ -86,7 +86,6 @@ struct GrassCollision : Feature virtual void PostPostLoad() override; - virtual bool SupportsVR() override { return true; }; struct Hooks { @@ -105,7 +104,7 @@ struct GrassCollision : Feature static void Install() { stl::write_vfunc<0x6, BSGrassShader_SetupGeometry>(RE::VTABLE_BSGrassShader[0]); - stl::write_thunk_call(REL::RelocationID(35565, 36564).address() + REL::Relocate(0x748, 0xC26, 0x7EE)); + stl::write_thunk_call(REL::RelocationID(35565, 36564).address() + REL::Relocate(0x748, 0xC26)); logger::info("[GRASS COLLISION] Installed hooks"); } }; diff --git a/src/Features/GrassLighting.h b/src/Features/GrassLighting.h index 61bacc67ec..1bece2fb1f 100644 --- a/src/Features/GrassLighting.h +++ b/src/Features/GrassLighting.h @@ -43,5 +43,4 @@ struct GrassLighting : Feature virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 086129f98e..545b0a950b 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -229,26 +229,6 @@ namespace func(a_this, a3, a_target, a_4, a_5); hdr->RestoreFramebuffer(); - // VR: RedirectFramebuffer made ISHDR write to hdrTexture (float16); after - // RestoreFramebuffer kFRAMEBUFFER reverts to its original texture. - // ISCopy reads kFRAMEBUFFER.SRV to distribute the frame to the HMD and - // companion window, so we must write the tonemapped content back into - // kFRAMEBUFFER before ISCopy runs. - // - // TODO (future HDR HMD support): The correct pipeline is to run the full - // HDR composite (PQ encode, paper white, peak nits) HERE, writing the - // result back to kFRAMEBUFFER so ISCopy distributes HDR-processed content - // to both the HMD and companion at their native sizes. The post-Present - // ApplyHDR path cannot do this correctly because ISCopy has already run - // and the companion back buffer (1024x1024) does not match outputTexture - // (sized from kMAIN). Requires hooking the ISCopy vfunc to fire - // HDROutputCS before distribution. - if (globals::game::isVR && hdr->settings.enableHDR && - hdr->hdrTexture && hdr->hdrTexture->resource) { - auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; - if (fb.texture) - globals::d3d::context->CopyResource(fb.texture, hdr->hdrTexture->resource.get()); - } } static inline REL::Relocation func; }; @@ -626,7 +606,7 @@ void HDRDisplay::PostPostLoad() if (!globals::features::upscaling.loaded) { logger::info("[HDR Display] Installing HDR pipeline hooks (Upscaling not loaded)"); stl::detour_thunk(REL::RelocationID(79947, 82084)); - stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7, 0x206)); + stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7)); } } @@ -868,13 +848,6 @@ bool HDRDisplay::ShouldUseD3D12UIBuffer() void HDRDisplay::SetUIBuffer() { - // VR: ISCopy reads kFRAMEBUFFER.SRV to distribute the frame to the HMD and - // companion window. Redirecting kFRAMEBUFFER.RTV here would cause vanilla UI - // to render into uiTexture instead, so ISCopy would send a UI-less frame to - // the HMD. Leave kFRAMEBUFFER alone; vanilla UI bakes directly into it. - if (globals::game::isVR) - return; - auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER]; // D3D12 swap chain path: route UI to uiBufferWrapped only when a compositor @@ -936,7 +909,7 @@ void HDRDisplay::SetUIBuffer() bool HDRDisplay::UsesDeferredPresentComposite() const { - return loaded && settings.enableHDR && !globals::game::isVR && + return loaded && settings.enableHDR && !globals::features::upscaling.d3d12SwapChainActive && uiTexture && uiTexture->rtv && hdrOutputCS; } @@ -977,7 +950,7 @@ namespace { static void WINAPI thunk(ID3D11DeviceContext* This, ID3D11BlendState* pBlendState, const FLOAT BlendFactor[4], UINT SampleMask) { - if (pBlendState && !globals::game::isVR) { + if (pBlendState) { auto& hdr = globals::features::hdrDisplay; const bool d3d11HdrCapture = hdr.loaded && hdr.settings.enableHDR && hdr.uiTexture; const bool fgCapture = globals::features::upscaling.d3d12SwapChainActive; @@ -1057,7 +1030,7 @@ void HDRDisplay::DrawImGuiForPresent(bool frameGenActive, bool hdrReady) if (frameGenActive) { auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER]; globals::d3d::context->OMSetRenderTargets(1, &data.RTV, nullptr); - } else if (hdrReady && !globals::game::isVR && uiTexture && uiTexture->rtv && uiTexture->resource) { + } else if (hdrReady && uiTexture && uiTexture->rtv && uiTexture->resource) { ID3D11RenderTargetView* uiRTV = uiTexture->rtv.get(); D3D11_TEXTURE2D_DESC texDesc{}; uiTexture->resource->GetDesc(&texDesc); @@ -1196,27 +1169,18 @@ void HDRDisplay::ApplyHDR() auto& framebufferRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; // Scene SRV selection: - // - VR: kFRAMEBUFFER at this point has scene + vanilla UI + ImGui all baked in - // (thunk restored hdrTexture→kFRAMEBUFFER, vanilla UI rendered on top, ImGui - // just rendered to kFRAMEBUFFER.RTV above). Use it directly so the companion - // window gets everything without a separate uiTexture capture pass. - // - Non-VR HDR: hdrTexture has float16 scene values >1.0 preserved from ISHDR. - // - Non-VR SDR: kFRAMEBUFFER has the tonemapped 0-1 ISHDR output. + // - HDR: hdrTexture has float16 scene values >1.0 preserved from ISHDR. + // - SDR: kFRAMEBUFFER has the tonemapped 0-1 ISHDR output. ID3D11ShaderResourceView* sceneSRV = - globals::game::isVR ? framebufferRT.SRV : (settings.enableHDR && hdrTexture && hdrTexture->srv) ? hdrTexture->srv.get() : framebufferRT.SRV; // Choose the correct UI buffer based on which path is active. - // VR uses the framebuffer directly, which already contains vanilla UI/ImGui. - // Binding a separate uiTexture here would duplicate the UI layer. ID3D11ShaderResourceView* uiSRV = nullptr; - if (!globals::game::isVR) { - if (upscaling.d3d12SwapChainActive && upscaling.dx12SwapChain.uiBufferWrapped) { - uiSRV = upscaling.dx12SwapChain.uiBufferWrapped->srv; - } else if (uiTexture && uiTexture->srv) { - uiSRV = uiTexture->srv.get(); - } + if (upscaling.d3d12SwapChainActive && upscaling.dx12SwapChain.uiBufferWrapped) { + uiSRV = upscaling.dx12SwapChain.uiBufferWrapped->srv; + } else if (uiTexture && uiTexture->srv) { + uiSRV = uiTexture->srv.get(); } ID3D11ShaderResourceView* views[2] = { sceneSRV, uiSRV }; diff --git a/src/Features/HDRDisplay.h b/src/Features/HDRDisplay.h index e5cc29b58f..41771236b8 100644 --- a/src/Features/HDRDisplay.h +++ b/src/Features/HDRDisplay.h @@ -20,7 +20,6 @@ struct HDRDisplay : public Feature virtual inline std::string GetShortName() override { return "HDRDisplay"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetCategory() const override { return "Display"; } - virtual inline bool SupportsVR() override { return false; } virtual inline bool IsCore() const override { return false; } virtual inline std::string_view GetShaderDefineName() override { return "HDR_OUTPUT"; } diff --git a/src/Features/HairSpecular.h b/src/Features/HairSpecular.h index 28fab821ec..147586e42b 100644 --- a/src/Features/HairSpecular.h +++ b/src/Features/HairSpecular.h @@ -59,5 +59,4 @@ struct HairSpecular : Feature virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; }; \ No newline at end of file diff --git a/src/Features/IBL.h b/src/Features/IBL.h index 5b153208e4..bef866d50f 100644 --- a/src/Features/IBL.h +++ b/src/Features/IBL.h @@ -3,7 +3,6 @@ struct IBL : Feature { public: - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; virtual inline std::string GetName() override { return "Image Based Lighting"; } diff --git a/src/Features/InteriorSun.cpp b/src/Features/InteriorSun.cpp index ca921845b9..3bb494d6bc 100644 --- a/src/Features/InteriorSun.cpp +++ b/src/Features/InteriorSun.cpp @@ -51,9 +51,9 @@ void InteriorSun::PostPostLoad() stl::write_thunk_call(REL::RelocationID(100852, 107642).address() + REL::Relocate(0x29E, 0x28F)); // Hooks and patch to enable directional lighting for interiors - stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x399, 0x37D, 0x639)); - stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x3AE, 0x392, 0x64E)); - REL::safe_fill(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x397, 0x37B, 0x637), REL::NOP, 2); + stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x399, 0x37D)); + stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x3AE, 0x392)); + REL::safe_fill(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x397, 0x37B), REL::NOP, 2); // Hook for overriding the rooms and portals passed to the directional light culling step to fix light leaking through unrendered geometry stl::detour_thunk(REL::RelocationID(101498, 108492)); @@ -67,11 +67,11 @@ void InteriorSun::PostPostLoad() gInteriorShadowDistance = reinterpret_cast(REL::RelocationID(513755, 391724).address()); // Patches BSShadowDirectionalLight::SetFrameCamera to read the correct shadow distance value in interior cells - const std::uintptr_t address = REL::RelocationID(101499, 108496).address() + REL::Relocate(0xD62, 0xE6C, 0xE72); + const std::uintptr_t address = REL::RelocationID(101499, 108496).address() + REL::Relocate(0xD62, 0xE6C); const std::int32_t displacement = static_cast(reinterpret_cast(gShadowDistance) - (address + 8)); REL::safe_write(address + 4, &displacement, sizeof(displacement)); - rasterStateCullMode = globals::game::isVR ? &globals::game::shadowState->GetVRRuntimeData().rasterStateCullMode : &globals::game::shadowState->GetRuntimeData().rasterStateCullMode; + rasterStateCullMode = &globals::game::shadowState->GetRuntimeData().rasterStateCullMode; logger::info("[Interior Sun] Installed hooks"); } diff --git a/src/Features/InteriorSun.h b/src/Features/InteriorSun.h index 28918e6769..11f2118ae6 100644 --- a/src/Features/InteriorSun.h +++ b/src/Features/InteriorSun.h @@ -21,7 +21,6 @@ struct InteriorSun : Feature virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; virtual void EarlyPrepass() override; diff --git a/src/Features/InverseSquareLighting.h b/src/Features/InverseSquareLighting.h index 803ebad13f..7a11099363 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -25,7 +25,6 @@ struct InverseSquareLighting : Feature inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }; - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/LODBlending.h b/src/Features/LODBlending.h index 9eb866ad02..0b4c1b1415 100644 --- a/src/Features/LODBlending.h +++ b/src/Features/LODBlending.h @@ -40,6 +40,5 @@ struct LODBlending : Feature virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index 4eddc810d2..e127ddce10 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -77,9 +77,7 @@ LightLimitFix::PerFrame LightLimitFix::GetCommonBufferData() void LightLimitFix::SetupResources() { - auto screenSize = globals::state->screenSize; - if (REL::Module::IsVR()) - screenSize.x *= .5; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; clusterSize[0] = ((uint)screenSize.x + 63) / 64; clusterSize[1] = ((uint)screenSize.y + 63) / 64; clusterSize[2] = 32; @@ -87,8 +85,6 @@ void LightLimitFix::SetupResources() { std::vector> clusterDefines; - if (REL::Module::IsVR()) - clusterDefines = { { "VR", "" } }; clusterBuildingCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\LightLimitFix\\ClusterBuildingCS.hlsl", clusterDefines, "cs_5_0"); clusterCullingCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\LightLimitFix\\ClusterCullingCS.hlsl", clusterDefines, "cs_5_0"); @@ -255,7 +251,7 @@ void LightLimitFix::BSLightingShader_SetupGeometry_GeometrySetupConstantPointLig if (i < a_pass->numShadowLights) { auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); + auto& maskIndex = shadowLight->GetRuntimeData().maskIndex; light.shadowMaskIndex = maskIndex; light.lightFlags.set(LightFlags::Shadow); } @@ -269,7 +265,7 @@ void LightLimitFix::BSLightingShader_SetupGeometry_GeometrySetupConstantPointLig if (!bsLight) continue; auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); + auto& maskIndex = shadowLight->GetRuntimeData().maskIndex; strictLightDataTemp.ShadowBitMask |= (1u << maskIndex); } } @@ -308,20 +304,18 @@ void LightLimitFix::BSLightingShader_SetupGeometry_After(RE::BSRenderPass*) void LightLimitFix::SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPoint3 a_initialPosition, bool a_cached) { - for (int eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { - RE::NiPoint3 eyePosition; + RE::NiPoint3 eyePosition; - if (a_cached) { - eyePosition = eyePositionCached[eyeIndex]; - } else { - eyePosition = Util::GetEyePosition(eyeIndex); - } - - auto worldPos = a_initialPosition - eyePosition; - a_light.positionWS[eyeIndex].data.x = worldPos.x; - a_light.positionWS[eyeIndex].data.y = worldPos.y; - a_light.positionWS[eyeIndex].data.z = worldPos.z; + if (a_cached) { + eyePosition = eyePositionCached; + } else { + eyePosition = Util::GetEyePosition(); } + + auto worldPos = a_initialPosition - eyePosition; + a_light.positionWS.data.x = worldPos.x; + a_light.positionWS.data.y = worldPos.y; + a_light.positionWS.data.z = worldPos.z; } void LightLimitFix::Prepass() @@ -377,8 +371,6 @@ void LightLimitFix::ClearShaderCache() clusterCullingCS = nullptr; } std::vector> clusterDefines; - if (REL::Module::IsVR()) - clusterDefines = { { "VR", "" } }; clusterBuildingCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\LightLimitFix\\ClusterBuildingCS.hlsl", clusterDefines, "cs_5_0"); clusterCullingCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\LightLimitFix\\ClusterCullingCS.hlsl", clusterDefines, "cs_5_0"); } @@ -390,11 +382,11 @@ void LightLimitFix::UpdateLights() auto shadowSceneNode = smState->shadowSceneNode[0]; - // Cache data since cameraData can become invalid in first-person + // Cache camera position from the FrameBuffer snapshot; shadowState::posAdjust can be stale in first-person - for (int eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { - auto eyePosition = globals::game::frameBufferCached.GetCameraPosAdjust(eyeIndex); - eyePositionCached[eyeIndex] = { eyePosition.x, eyePosition.y, eyePosition.z }; + { + auto eyePosition = globals::game::frameBufferCached.GetCameraPosAdjust(); + eyePositionCached = { eyePosition.x, eyePosition.y, eyePosition.z }; } eastl::vector lightsData{}; @@ -449,7 +441,7 @@ void LightLimitFix::UpdateLights() if (bsLight->IsShadowLight()) { auto* shadowLight = static_cast(bsLight); - GET_INSTANCE_MEMBER(maskIndex, shadowLight); + auto& maskIndex = shadowLight->GetRuntimeData().maskIndex; light.shadowMaskIndex = maskIndex; light.lightFlags.set(LightFlags::Shadow); } @@ -494,9 +486,7 @@ void LightLimitFix::UpdateStructure() lightsNear = *globals::game::cameraNear; lightsFar = *globals::game::cameraFar; - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - if (REL::Module::IsVR()) - renderSize.x *= .5; + auto renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); clusterSize[0] = ((uint)renderSize.x + 63) / 64; clusterSize[1] = ((uint)renderSize.y + 63) / 64; clusterSize[2] = 32; diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index 0dc60f4c1d..07205ffab6 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -18,8 +18,7 @@ struct LightLimitFix : OverlayFeature { T("feature.light_limit_fix.key_feature_1", "Removes 4-light limit"), T("feature.light_limit_fix.key_feature_2", "Unlimited dynamic lights"), T("feature.light_limit_fix.key_feature_3", "Improved lighting quality"), - T("feature.light_limit_fix.key_feature_4", "Enhanced visual realism"), - T("feature.light_limit_fix.key_feature_5", "Enhanced visual realism") } }; + T("feature.light_limit_fix.key_feature_4", "Enhanced visual realism") } }; }; bool HasShaderDefine(RE::BSShader::Type) override { return true; }; @@ -50,7 +49,7 @@ struct LightLimitFix : OverlayFeature float invRadius; float fadeZone; float sizeBias; - PositionOpt positionWS[2]; + PositionOpt positionWS; uint128_t roomFlags = uint32_t(0); stl::enumeration lightFlags; uint32_t shadowMaskIndex = 0; @@ -115,7 +114,6 @@ struct LightLimitFix : OverlayFeature ConstantBuffer* strictLightDataCB = nullptr; - int eyeCount = !REL::Module::IsVR() ? 1 : 2; bool previousEnableLightsVisualisation = settings.EnableLightsVisualisation; bool currentEnableLightsVisualisation = settings.EnableLightsVisualisation; @@ -135,7 +133,7 @@ struct LightLimitFix : OverlayFeature float lightsNear = 1; float lightsFar = 16384; - RE::NiPoint3 eyePositionCached[2]{}; + RE::NiPoint3 eyePositionCached{}; bool wasEmpty = false; bool wasWorld = false; int previousRoomIndex = -1; @@ -224,14 +222,13 @@ struct LightLimitFix : OverlayFeature stl::write_vfunc<0x6, BSWaterShader_SetupGeometry>(RE::VTABLE_BSWaterShader[0]); stl::write_thunk_call(REL::RelocationID(100994, 107781).address() + 0x92); - stl::write_thunk_call(REL::RelocationID(100997, 107784).address() + REL::Relocate(0x139, 0x12A, 0x133)); + stl::write_thunk_call(REL::RelocationID(100997, 107784).address() + REL::Relocate(0x139, 0x12A)); stl::write_thunk_call(REL::RelocationID(101296, 108283).address() + REL::Relocate(0xB7, 0x7E)); logger::info("[LLF] Installed hooks"); } }; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; } }; @@ -261,10 +258,10 @@ struct fmt::formatter auto format(const LightLimitFix::LightData& l, format_context& ctx) const -> format_context::iterator { // ctx.out() is an output iterator to write to. - return fmt::format_to(ctx.out(), "{{address {:x} color {} radius {} posWS {} {}}}", + return fmt::format_to(ctx.out(), "{{address {:x} color {} radius {} posWS {}}}", reinterpret_cast(&l), (Vector3)l.color, l.radius, - (Vector3)l.positionWS[0].data, (Vector3)l.positionWS[1].data); + (Vector3)l.positionWS.data); } }; diff --git a/src/Features/LinearLighting.cpp b/src/Features/LinearLighting.cpp index ce3ead1a31..ffed1a9d81 100644 --- a/src/Features/LinearLighting.cpp +++ b/src/Features/LinearLighting.cpp @@ -117,7 +117,7 @@ void LinearLighting::Prepass() if (!imageSpaceManager) return; - dirLightMult = !globals::game::isVR ? imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale : imageSpaceManager->GetVRRuntimeData().data.baseData.hdr.sunlightScale; + dirLightMult = imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale; } struct LinearLighting::Hooks diff --git a/src/Features/LinearLighting.h b/src/Features/LinearLighting.h index a3e30ade1c..2195154192 100644 --- a/src/Features/LinearLighting.h +++ b/src/Features/LinearLighting.h @@ -20,7 +20,6 @@ struct LinearLighting : Feature T("feature.linear_lighting.key_feature_3", "Makes PBR really work") } }; }; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; struct Settings diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index d09ba40818..e3e3872bda 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -123,7 +123,6 @@ struct PerformanceOverlay : OverlayFeature std::string GetName() override { return "Performance Overlay"; } virtual std::string GetDisplayName() override { return T("feature.performance_overlay.name", "Performance Overlay"); } std::string GetShortName() override { return "PerformanceOverlay"; } - virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } virtual bool IsInMenu() const override { return true; } bool IsOverlayVisible() const override { return settings.ShowInOverlay; } diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index a5b093f2f0..920cba3f49 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -62,7 +62,6 @@ class RenderDoc : public Feature T("feature.render_doc.key_feature_2", "Open captures folder"), T("feature.render_doc.key_feature_3", "Capture file management") } }; } - bool SupportsVR() override { return true; } std::string_view GetShaderDefineName() override { return ""; } bool HasShaderDefine(RE::BSShader::Type) override { return false; }; diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index a232895eda..1e4c1b2df7 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -69,13 +69,9 @@ void ScreenSpaceGI::DrawSettings() } ImGui::TableNextColumn(); { - auto vanillaSSAOGuard = Util::DisableGuard(globals::game::isVR); ImGui::Checkbox(T(TKEY("vanilla_ssao"), "Vanilla SSAO"), &settings.EnableVanillaSSAO); if (auto _tt = Util::HoverTooltipWrapper()) { - if (globals::game::isVR) - ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip_vr"), "Vanilla SSAO is not supported in VR.")); - else - ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip"), "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.")); + ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip"), "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.")); } } ImGui::TableNextColumn(); @@ -95,18 +91,16 @@ void ScreenSpaceGI::DrawSettings() auto qualityGuard = Util::DisableGuard(!settings.Enabled); if (ImGui::BeginTable("Presets", 5)) { - auto select = [](auto flatVal, auto vrVal) { return globals::game::isVR ? vrVal : flatVal; }; - ImGui::TableNextColumn(); if (ImGui::Button(T(TKEY("ao_only"), "AO only"), { -1, 0 })) { - settings.NumSlices = select(1, 3); - settings.NumSteps = select(6, 8); + settings.NumSlices = 1; + settings.NumSteps = 6; settings.EnableBlur = true; settings.EnableGI = false; recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text(select("1 Slice, 6 Steps, blur enabled, no GI\n", "3 Slices, 8 Steps, blur enabled, no GI\n")); + ImGui::Text("1 Slice, 6 Steps, blur enabled, no GI\n"); } ImGui::TableNextColumn(); @@ -587,7 +581,7 @@ void ScreenSpaceGI::SetupResources() void ScreenSpaceGI::ClearShaderCache() { static const std::vector*> shaderPtrs = { - &prefilterDepthsCompute, &prefilterRadianceCompute, &prefilterNormalCompute, &radianceDisoccCompute, &giCompute, &blurCompute, &stereoSyncCompute, &upsampleCompute + &prefilterDepthsCompute, &prefilterRadianceCompute, &prefilterNormalCompute, &radianceDisoccCompute, &giCompute, &blurCompute, &upsampleCompute }; for (auto shader : shaderPtrs) @@ -616,11 +610,7 @@ void ScreenSpaceGI::CompileComputeShaders() { &upsampleCompute, "upsample.cs.hlsl", {} }, }; - if (REL::Module::IsVR()) - shaderInfos.push_back({ &stereoSyncCompute, "stereoSync.cs.hlsl", { { "FRAMEBUFFER", "" } } }); for (auto& info : shaderInfos) { - if (REL::Module::IsVR()) - info.defines.push_back({ "VR", "" }); if (settings.ResolutionMode == 1) info.defines.push_back({ "HALF_RES", "" }); if (settings.ResolutionMode == 2) @@ -653,20 +643,18 @@ void ScreenSpaceGI::UpdateSB() float2 dynres = Util::ConvertToDynamic(res); dynres = { floor(dynres.x), floor(dynres.y) }; - static float4x4 prevInvView[2] = {}; + static float4x4 prevInvView = {}; SSGICB data; { - for (int eyeIndex = 0; eyeIndex < (1 + REL::Module::IsVR()); ++eyeIndex) { - auto eye = Util::GetCameraData(eyeIndex); + { + auto eye = globals::game::shadowState->GetRuntimeData().cameraData.getEye(); - data.PrevInvViewMat[eyeIndex] = prevInvView[eyeIndex]; - data.NDCToViewMul[eyeIndex] = { 2.0f / eye.projMat(0, 0), -2.0f / eye.projMat(1, 1) }; - data.NDCToViewAdd[eyeIndex] = { -1.0f / eye.projMat(0, 0), 1.0f / eye.projMat(1, 1) }; - if (REL::Module::IsVR()) - data.NDCToViewMul[eyeIndex].x *= 2; + data.PrevInvViewMat = prevInvView; + data.NDCToViewMul = { 2.0f / eye.projMat(0, 0), -2.0f / eye.projMat(1, 1), 0.0f, 0.0f }; + data.NDCToViewAdd = { -1.0f / eye.projMat(0, 0), 1.0f / eye.projMat(1, 1), 0.0f, 0.0f }; - prevInvView[eyeIndex] = eye.viewMat.Invert(); + prevInvView = eye.viewMat.Invert(); } data.TexDim = res; @@ -708,7 +696,7 @@ void ScreenSpaceGI::DrawSSGI() auto context = globals::d3d::context; auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - GET_INSTANCE_MEMBER(BSImagespaceShaderISSAOBlurH, imageSpaceManager); + auto& BSImagespaceShaderISSAOBlurH = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISSAOBlurH; // Toggle vanilla SSAO static bool* enableSSAO = reinterpret_cast(reinterpret_cast(BSImagespaceShaderISSAOBlurH.get()) + 0x50LL); @@ -744,7 +732,7 @@ void ScreenSpaceGI::DrawSSGI() auto rts = renderer->GetRuntimeData().renderTargets; auto deferred = globals::deferred; - float2 size = Util::ConvertToDynamic(globals::state->screenSize); + float2 size = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); auto resolution = std::array{ (uint)size.x, (uint)size.y }; auto resChoices = std::array{ resolution, std::array{ resolution[0] >> 1, resolution[1] >> 1 }, std::array{ resolution[0] >> 2, resolution[1] >> 2 } @@ -926,38 +914,6 @@ void ScreenSpaceGI::DrawSSGI() lastFrameAccumTexIdx = !lastFrameAccumTexIdx; } - // VR stereo sync: bilateral blend of SSGI buffers between eyes - // Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" - if (REL::Module::IsVR() && stereoSyncCompute) { - TracyD3D11Zone(globals::state->tracyCtx, "SSGI - Stereo Sync"); - - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("SSGI - Stereo Sync"); - - resetViews(); - srvs.at(0) = texWorkingDepth->srv.get(); - srvs.at(1) = texAo[inputAoTexIdx]->srv.get(); - srvs.at(2) = texIlY[inputGITexIdx]->srv.get(); - srvs.at(3) = texIlCoCg[inputGITexIdx]->srv.get(); - - uavs.at(0) = texAo[!inputAoTexIdx]->uav.get(); - uavs.at(1) = texIlY[!inputGITexIdx]->uav.get(); - uavs.at(2) = texIlCoCg[!inputGITexIdx]->uav.get(); - - context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); - context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); - context->CSSetShader(stereoSyncCompute.get(), nullptr, 0); - globals::profiler->BeginPass("ScreenSpaceGI::StereoSync"); - context->Dispatch((internalRes[0] + 7u) >> 3, (internalRes[1] + 7u) >> 3, 1); - globals::profiler->EndPass(); - - inputAoTexIdx = !inputAoTexIdx; - inputGITexIdx = !inputGITexIdx; - - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); - } - // upsample if (settings.ResolutionMode != 0) { resetViews(); diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index e2dddc30de..96a05f588c 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -8,7 +8,6 @@ struct ScreenSpaceGI : Feature static constexpr std::string_view MOD_ID = "130375"; public: - bool inline SupportsVR() override { return true; } virtual inline std::string GetName() override { return "Screen Space GI"; } virtual std::string GetDisplayName() override { return T("feature.screen_space_gi.name", "Screen Space GI"); } @@ -19,9 +18,6 @@ struct ScreenSpaceGI : Feature virtual std::pair> GetFeatureSummary() override { std::string desc = T("feature.screen_space_gi.description", "Screen Space Global Illumination adds realistic indirect lighting and ambient occlusion to the game. This technique simulates how light bounces off surfaces to illuminate other objects naturally."); - if (REL::Module::IsVR()) { - desc += T("feature.screen_space_gi.vr_warning", "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects."); - } return { desc, { T("feature.screen_space_gi.key_feature_1", "Realistic indirect lighting"), T("feature.screen_space_gi.key_feature_2", "Enhanced ambient occlusion"), @@ -53,12 +49,12 @@ struct ScreenSpaceGI : Feature struct Settings { bool Enabled = true; - bool EnableGI = REL::Module::IsVR() ? false : true; // AO only for VR by default + bool EnableGI = true; bool EnableExperimentalSpecularGI = false; bool EnableVanillaSSAO = false; // performance/quality - uint NumSlices = REL::Module::IsVR() ? 3u : 4u; // AO preset for VR - uint NumSteps = REL::Module::IsVR() ? 6u : 8u; + uint NumSlices = 4u; + uint NumSteps = 8u; int ResolutionMode = 1; // 0-full, 1-half, 2-quarter - DBF default // visual float MinScreenRadius = 0.01f; @@ -84,9 +80,9 @@ struct ScreenSpaceGI : Feature struct alignas(16) SSGICB { - float4x4 PrevInvViewMat[2]; - float2 NDCToViewMul[2]; - float2 NDCToViewAdd[2]; + float4x4 PrevInvViewMat; + float4 NDCToViewMul; + float4 NDCToViewAdd; float2 TexDim; float2 RcpTexDim; // @@ -160,6 +156,5 @@ struct ScreenSpaceGI : Feature winrt::com_ptr radianceDisoccCompute = nullptr; winrt::com_ptr giCompute = nullptr; winrt::com_ptr blurCompute = nullptr; - winrt::com_ptr stereoSyncCompute = nullptr; winrt::com_ptr upsampleCompute = nullptr; }; diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index c5452d91fe..cf3b6621ca 100644 --- a/src/Features/ScreenSpaceShadows.cpp +++ b/src/Features/ScreenSpaceShadows.cpp @@ -45,15 +45,6 @@ void ScreenSpaceShadows::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("%s", T(TKEY("shadow_contrast_tooltip"), "Contrast boost for the shadow transition. Higher values produce harder shadow edges.")); - if (globals::game::isVR && globals::state->IsDeveloperMode()) { - ImGui::Checkbox(T(TKEY("vr_stereo_sync"), "VR Stereo Sync"), &enableStereoSync); - if (auto _tt = Util::HoverTooltipWrapper()) - ImGui::Text("%s", T(TKEY("vr_stereo_sync_tooltip"), - "Synchronizes shadow data between left and right eyes via bilateral reprojection " - "and applies a depth-weighted blur to reduce per-eye noise. " - "Uses min-blend so if either eye detects an occluder, the shadow is preserved. ")); - } - ImGui::Spacing(); ImGui::Spacing(); ImGui::TreePop(); @@ -66,31 +57,16 @@ void ScreenSpaceShadows::InvalidateRaymarchShaders() raymarchCS->Release(); raymarchCS = nullptr; } - if (raymarchRightCS) { - raymarchRightCS->Release(); - raymarchRightCS = nullptr; - } } void ScreenSpaceShadows::ClearShaderCache() { InvalidateRaymarchShaders(); - if (stereoSyncCS) { - stereoSyncCS->Release(); - stereoSyncCS = nullptr; - } } uint ScreenSpaceShadows::GetScaledSampleCount() { - if (globals::game::isVR) { - // In VR, SAMPLE_COUNT is a pixel-space ray length that is FOV-driven, not resolution-driven. - // Resolution-scaling produced 2-8x excess samples at VR resolutions with no quality benefit. - // WAVE_SIZE (64) alignment is required for correct Bend READ_COUNT computation. - return bendSettings.SampleCount * 64; - } - - float2 renderSize = Util::ConvertToDynamic(globals::state->screenSize); + float2 renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); // Scale sample count based on both dimensions relative to 1920x1080 reference float2 referenceRes = { 1920.0f, 1080.0f }; @@ -127,24 +103,10 @@ ID3D11ComputeShader* ScreenSpaceShadows::GetComputeRaymarch() return raymarchCS; } -ID3D11ComputeShader* ScreenSpaceShadows::GetComputeRaymarchRight() -{ - if (!raymarchRightCS) { - uint scaledSampleCount = GetScaledSampleCount(); - auto sampleCount = std::format("{}", scaledSampleCount); - std::vector> defines{ { "SAMPLE_COUNT", sampleCount.c_str() }, { "RIGHT", "" } }; - if (globals::features::terrainBlending.loaded) - defines.push_back({ "TERRAIN_BLENDING", "" }); - raymarchRightCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\ScreenSpaceShadows\\RaymarchCS.hlsl", defines, "cs_5_0"); - } - return raymarchRightCS; -} - void ScreenSpaceShadows::DrawShadows() { ZoneScopedS(8); - auto state = globals::state; - TracyD3D11Zone(state->tracyCtx, "Screen Space Shadows"); + TracyD3D11Zone(globals::state->tracyCtx, "Screen Space Shadows"); auto context = globals::d3d::context; @@ -156,21 +118,18 @@ void ScreenSpaceShadows::DrawShadows() light.Normalize(); float4 lightProjection = float4(-light.x, -light.y, -light.z, 0.0f); - // Helper lambda to calculate light projection for a given eye - auto CalculateLightProjection = [&](uint32_t eyeIndex = 0) -> std::array { - auto viewProjMat = globals::game::frameBufferCached.GetCameraViewProj(eyeIndex).Transpose(); + // Helper lambda to calculate light projection + auto CalculateLightProjection = [&]() -> std::array { + auto viewProjMat = globals::game::frameBufferCached.GetCameraViewProj().Transpose(); auto projectedLight = DirectX::SimpleMath::Vector4::Transform(lightProjection, viewProjMat); return { projectedLight.x, projectedLight.y, projectedLight.z, projectedLight.w }; }; - auto lightProjectionF = CalculateLightProjection(0); + auto lightProjectionF = CalculateLightProjection(); - float2 renderSize = Util::ConvertToDynamic(state->screenSize); + float2 renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); int viewportSize[2] = { (int)renderSize.x, (int)renderSize.y }; - if (globals::game::isVR) - viewportSize[0] /= 2; - int minRenderBounds[2] = { 0, 0 }; int maxRenderBounds[2] = { viewportSize[0], viewportSize[1] }; @@ -195,16 +154,12 @@ void ScreenSpaceShadows::DrawShadows() float2 dynamicRes = { viewport->GetRuntimeData().dynamicResolutionWidthRatio, viewport->GetRuntimeData().dynamicResolutionHeightRatio }; - // Shared dispatch logic for both VR and non-VR - auto DispatchEye = [&](const char* eyeName, ID3D11ComputeShader* shader, const float* lightProj, - float invTexSizeX, float invTexSizeY) { - std::string timerName = eyeName ? std::format("ScreenSpaceShadows::RayMarch({})", eyeName) : "ScreenSpaceShadows::RayMarch"; - globals::profiler->BeginPass(timerName); + // Shared dispatch logic + auto Dispatch = [&](ID3D11ComputeShader* shader, const float* lightProj, + float invTexSizeX, float invTexSizeY) { + globals::profiler->BeginPass("ScreenSpaceShadows::RayMarch"); - if (globals::state->frameAnnotations && eyeName) { - std::string eventName = std::format("SSS - Ray March ({})", eyeName); - globals::state->BeginPerfEvent(eventName); - } else if (globals::state->frameAnnotations) { + if (globals::state->frameAnnotations) { globals::state->BeginPerfEvent("SSS - Ray March"); } @@ -216,7 +171,7 @@ void ScreenSpaceShadows::DrawShadows() auto dispatchData = dispatchList.Dispatch[i]; { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - DispatchEye CB"); + TracyD3D11Zone(globals::state->tracyCtx, "SSS - Dispatch CB"); RaymarchCB data{}; data.LightCoordinate[0] = dispatchList.LightCoordinate_Shader[0]; @@ -241,7 +196,7 @@ void ScreenSpaceShadows::DrawShadows() } { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - DispatchEye Sweep"); + TracyD3D11Zone(globals::state->tracyCtx, "SSS - Dispatch Sweep"); context->Dispatch(dispatchData.WaveCount[0], dispatchData.WaveCount[1], dispatchData.WaveCount[2]); } } @@ -256,21 +211,7 @@ void ScreenSpaceShadows::DrawShadows() float InvTexSizeX = 1.0f / (float)viewportSize[0]; float InvTexSizeY = 1.0f / (float)viewportSize[1]; - if (!globals::game::isVR) { - DispatchEye(nullptr, GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); - } else { - { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - Left Eye"); - DispatchEye("Left Eye", GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); - } - - // Calculate light projection for right eye - auto lightProjectionRightF = CalculateLightProjection(1); - { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - Right Eye"); - DispatchEye("Right Eye", GetComputeRaymarchRight(), lightProjectionRightF.data(), InvTexSizeX, InvTexSizeY); - } - } + Dispatch(GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); ID3D11ShaderResourceView* views[1]{ nullptr }; context->CSSetShaderResources(0, 1, views); @@ -287,73 +228,6 @@ void ScreenSpaceShadows::DrawShadows() context->CSSetConstantBuffers(1, 1, &buffer); } -void ScreenSpaceShadows::DrawStereoSync() -{ - if (!globals::game::isVR || !enableStereoSync || !stereoSyncCopyTex || !stereoSyncCB) - return; - - if (!stereoSyncCS) { - std::vector> defines{ { "VR", "" }, { "FRAMEBUFFER", "" } }; - if (globals::features::terrainBlending.loaded) - defines.push_back({ "TERRAIN_BLENDING", "" }); - stereoSyncCS = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\ScreenSpaceShadows\\StereoSyncCS.hlsl", defines, "cs_5_0")); - } - if (!stereoSyncCS) - return; - - ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "SSS - Stereo Sync"); - - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("SSS - Stereo Sync"); - - auto context = globals::d3d::context; - globals::profiler->BeginPass("ScreenSpaceShadows::StereoSync"); - - context->CopyResource(stereoSyncCopyTex->resource.get(), screenSpaceShadowsTexture->resource.get()); - - float2 resolution = Util::ConvertToDynamic(globals::state->screenSize); - - StereoSyncCB cbData{}; - cbData.FrameDim[0] = resolution.x; - cbData.FrameDim[1] = resolution.y; - cbData.RcpFrameDim[0] = 1.0f / resolution.x; - cbData.RcpFrameDim[1] = 1.0f / resolution.y; - - stereoSyncCB->Update(cbData); - auto cbPtr = stereoSyncCB->CB(); - - // Same 24/32-bit depth path as the raymarch — SrcDepthTexture's HLSL type is - // conditional on TERRAIN_BLENDING via the define passed at compile time below. - auto* depthSRV = Util::GetCurrentSceneDepthSRV(false); - ID3D11ShaderResourceView* srvs[2]{ depthSRV, stereoSyncCopyTex->srv.get() }; - ID3D11UnorderedAccessView* uavs[1]{ screenSpaceShadowsTexture->uav.get() }; - - context->CSSetConstantBuffers(1, 1, &cbPtr); - auto* sharedDataBuf = globals::state->sharedDataCB->CB(); - context->CSSetConstantBuffers(5, 1, &sharedDataBuf); - context->CSSetShaderResources(0, 2, srvs); - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - context->CSSetShader(stereoSyncCS, nullptr, 0); - - auto dispatchCount = Util::GetScreenDispatchCount(true); - context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - - srvs[0] = nullptr; - srvs[1] = nullptr; - uavs[0] = nullptr; - cbPtr = nullptr; - context->CSSetShaderResources(0, 2, srvs); - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - context->CSSetConstantBuffers(1, 1, &cbPtr); - context->CSSetShader(nullptr, nullptr, 0); - - globals::profiler->EndPass(); - - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); -} - void ScreenSpaceShadows::Prepass() { auto context = globals::d3d::context; @@ -364,7 +238,6 @@ void ScreenSpaceShadows::Prepass() if (auto sky = globals::game::sky) if (bendSettings.Enable && sky->mode.get() == RE::Sky::Mode::kFull) { DrawShadows(); - DrawStereoSync(); } auto view = screenSpaceShadowsTexture->srv.get(); @@ -395,10 +268,6 @@ void ScreenSpaceShadows::SetupResources() { raymarchCB = new ConstantBuffer(ConstantBufferDesc(), "SSS::RaymarchCB"); - if (globals::game::isVR) { - stereoSyncCB = new ConstantBuffer(ConstantBufferDesc(), "SSS::StereoSyncCB"); - } - { auto device = globals::d3d::device; @@ -441,11 +310,6 @@ void ScreenSpaceShadows::SetupResources() screenSpaceShadowsTexture = new Texture2D(texDesc, "SSS::ShadowTexture"); screenSpaceShadowsTexture->CreateSRV(srvDesc); screenSpaceShadowsTexture->CreateUAV(uavDesc); - - if (globals::game::isVR) { - stereoSyncCopyTex = new Texture2D(texDesc, "SSS::StereoSyncCopy"); - stereoSyncCopyTex->CreateSRV(srvDesc); - } } } #undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenSpaceShadows.h b/src/Features/ScreenSpaceShadows.h index 48c79556c1..fadd8658dc 100644 --- a/src/Features/ScreenSpaceShadows.h +++ b/src/Features/ScreenSpaceShadows.h @@ -25,9 +25,9 @@ struct ScreenSpaceShadows : Feature struct BendSettings { - float SurfaceThickness = !globals::game::isVR ? 0.02f : 0.010f; + float SurfaceThickness = 0.02f; float BilinearThreshold = 0.02f; - float ShadowContrast = !globals::game::isVR ? 1.0f : 4.0f; + float ShadowContrast = 1.0f; uint Enable = 1; uint SampleCount = 1; uint pad0[3]; @@ -56,28 +56,13 @@ struct ScreenSpaceShadows : Feature }; STATIC_ASSERT_ALIGNAS_16(RaymarchCB); - bool enableStereoSync = true; - - struct alignas(16) StereoSyncCB - { - float FrameDim[2]; - float RcpFrameDim[2]; - }; - STATIC_ASSERT_ALIGNAS_16(StereoSyncCB); - ID3D11SamplerState* pointBorderSampler = nullptr; ConstantBuffer* raymarchCB = nullptr; ID3D11ComputeShader* raymarchCS = nullptr; - ID3D11ComputeShader* raymarchRightCS = nullptr; Texture2D* screenSpaceShadowsTexture = nullptr; - // VR stereo sync resources - Texture2D* stereoSyncCopyTex = nullptr; - ConstantBuffer* stereoSyncCB = nullptr; - ID3D11ComputeShader* stereoSyncCS = nullptr; - virtual void SetupResources() override; virtual void DrawSettings() override; @@ -87,7 +72,6 @@ struct ScreenSpaceShadows : Feature uint GetScaledSampleCount(); uint lastCompiledSampleCount = 0; ID3D11ComputeShader* GetComputeRaymarch(); - ID3D11ComputeShader* GetComputeRaymarchRight(); virtual void Prepass() override; @@ -95,9 +79,7 @@ struct ScreenSpaceShadows : Feature virtual void SaveSettings(json& o_json) override; void DrawShadows(); - void DrawStereoSync(); virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index 39f7aaf5bb..bf57ba97c7 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -1,5 +1,5 @@ // Screenshot Feature -// Non-blocking screenshot tool for flat (SE/AE) and VR. GPU copy runs on the +// Non-blocking screenshot tool. GPU copy runs on the // render thread; encoding and disk I/O run on a dedicated worker thread so // capture does not stall the frame. @@ -17,7 +17,10 @@ #define I18N_KEY_PREFIX "feature.screenshot." #include +#pragma warning(push) +#pragma warning(disable : 4244) // double->float conversion in third-party header #include +#pragma warning(pop) #include #include @@ -340,13 +343,11 @@ namespace bool IsFlatHdrScreenshotCapture() { - return !globals::game::isVR && - globals::features::hdrDisplay.loaded && + return globals::features::hdrDisplay.loaded && globals::features::hdrDisplay.settings.enableHDR; } // Picks the capture source: - // VR -> kVR_FRAMEBUFFER (SBS). // HDR enabled -> swap-chain back buffer after ApplyHDR (PQ HDR10 / PQ float). // otherwise -> kFRAMEBUFFER (tonemapped UNORM). CaptureSource SelectCaptureSource(winrt::com_ptr& holder) @@ -357,13 +358,6 @@ namespace return src; } - if (globals::game::isVR) { - auto& slot = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kVR_FRAMEBUFFER]; - src.texture = ResolveSlotTexture(slot, holder); - src.srv = slot.SRV; - src.description = "VR SBS framebuffer"; - return src; - } if (IsFlatHdrScreenshotCapture()) { src.texture = ResolveDisplayedBackBuffer(holder); @@ -396,11 +390,9 @@ namespace // 1. BlendEnable must stay FALSE - the source texture carries non-1 alpha // where Skyrim composited UI plates; default SRC_ALPHA blend lets the // host window background show through (visible on the desktop mirror). - // 2. WriteMask must exclude alpha (RGB only). In VR, Skyrim's menu UI - // shader recomposites our menu plate over the SBS framebuffer with - // alpha blending; writing texture alpha into the menu plate RT - // produces a cutout visible only through the HMD. RGB-only writes - // leave the plate's pre-cleared alpha=1 in place. + // 2. WriteMask must exclude alpha (RGB only) to avoid compositing + // artifacts. RGB-only writes leave the plate's pre-cleared alpha=1 + // in place. // Paired with ImDrawCallback_ResetRenderState queued by Subrect::DrawEditor // immediately after the image draw. void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) @@ -569,17 +561,6 @@ bool ScreenshotFeature::IsInMenu() const void ScreenshotFeature::PostPostLoad() { - // Seed VR-specific presets here rather than in LoadSettings: Feature::Load - // only dispatches to LoadSettings when the JSON already has a settings - // block, so a fresh install would skip a seed placed there. Left first so - // it's the initial selection (matches vanilla Skyrim VR's left-eye save). - if (REL::Module::IsVR()) { - subrect.SeedDefaultPresets({ - { .name = "Left Eye", .uv = { 0.0f, 0.0f, 0.5f, 1.0f } }, - { .name = "Right Eye", .uv = { 0.5f, 0.0f, 0.5f, 1.0f } }, - { .name = "Full Frame", .uv = { 0.0f, 0.0f, 1.0f, 1.0f } }, - }); - } } void ScreenshotFeature::LoadSettings(json& a_json) @@ -637,7 +618,7 @@ void ScreenshotFeature::DrawSettings() ImGui::TextWrapped("%s", T(TKEY("sdr_note"), "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. " - "SDR and VR captures use the lossless format selected below.")); + "SDR captures use the lossless format selected below.")); } if (ImGui::Button(T(TKEY("take_screenshot"), "Take Screenshot Now"))) { @@ -652,15 +633,12 @@ void ScreenshotFeature::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Places the saved screenshot on the clipboard as a file (paste in Explorer or attach in chat apps)."); - if (!hdrCaptureAvailable || globals::game::isVR) { + if (!hdrCaptureAvailable) { int sdrFormat = sdrUsePng ? 1 : 0; ImGui::RadioButton("BMP (lossless)", &sdrFormat, 0); ImGui::SameLine(); ImGui::RadioButton("PNG (lossless)", &sdrFormat, 1); sdrUsePng = sdrFormat != 0; - if (hdrCaptureAvailable && globals::game::isVR) { - ImGui::TextWrapped("VR captures use this format. Flat HDR mode always saves HDR PNG."); - } } char buf[260]; @@ -703,8 +681,7 @@ void ScreenshotFeature::DrawSettings() ImGui::SeparatorText(T(TKEY("crop"), "Crop")); - // Preview reflects what Capture() would save. Full source frame so VR users - // can drag-crop across the eye boundary if a seeded preset doesn't fit. + // Preview reflects what Capture() would save. winrt::com_ptr previewTextureKeepAlive; const auto src = SelectCaptureSource(previewTextureKeepAlive); diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index c5a87cb8f7..67381d2d89 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -18,7 +18,6 @@ struct ScreenshotFeature : public Feature virtual std::string GetShortName() override { return "Screenshot"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kUtility; } - virtual bool SupportsVR() override { return true; } virtual bool IsInMenu() const override; virtual void DrawSettings() override; @@ -36,7 +35,7 @@ struct ScreenshotFeature : public Feature std::string screenshotPath = "Screenshots"; // HDR PNG quantization (7-16); used when HDR Display captures the back buffer. unsigned int hdrPngBitDepth = 11; - // SDR / VR output (HDR captures always use PNG). + // SDR output (HDR captures always use PNG). bool sdrUsePng = false; // After save, put the file path on the clipboard (CF_HDROP). bool copyToClipboard = false; diff --git a/src/Features/Skin.h b/src/Features/Skin.h index 9e21044810..b5af9053d6 100644 --- a/src/Features/Skin.h +++ b/src/Features/Skin.h @@ -30,7 +30,6 @@ struct Skin : Feature return t == RE::BSShader::Type::Lighting; }; - virtual inline bool SupportsVR() { return true; } virtual void RestoreDefaultSettings() override; virtual void DrawSettings() override; diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 2290fe1819..779fb31988 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -50,7 +50,6 @@ struct SkySync : Feature virtual void RestoreDefaultSettings() override; virtual bool IsCore() const override { return true; } - virtual bool SupportsVR() override { return true; } void OnSkyUpdateColors(RE::Sky* sky); diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index b52305700c..6a44d86b0d 100644 --- a/src/Features/Skylighting.cpp +++ b/src/Features/Skylighting.cpp @@ -174,7 +174,7 @@ Skylighting::SkylightingCB Skylighting::GetCommonBufferData(bool a_inWorld) static float3 prevCellID = { 0, 0, 0 }; - auto eyePosNI = Util::GetEyePosition(0); + auto eyePosNI = Util::GetEyePosition(); auto eyePos = float3{ eyePosNI.x, eyePosNI.y, eyePosNI.z }; float3 cellSize = { @@ -259,12 +259,9 @@ void Skylighting::PostPostLoad() { logger::info("[SKYLIGHTING] Hooking BSLightingShaderProperty::GetPrecipitationOcclusionMapRenderPassesImp"); stl::write_vfunc<0x2D, BSLightingShaderProperty_GetPrecipitationOcclusionMapRenderPassesImpl>(RE::VTABLE_BSLightingShaderProperty[0]); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1)); - if (REL::Module::IsVR()) - stl::write_thunk_call(REL::RelocationID(25643, 26185).address() + REL::Relocate(0x5D9, 0x59D, 0x5DC)); - else - stl::write_thunk_call(REL::RelocationID(25643, 26185).address() + REL::Relocate(0x5D9, 0x59D, 0x5DC)); + stl::write_thunk_call(REL::RelocationID(25643, 26185).address() + REL::Relocate(0x5D9, 0x59D)); MenuOpenCloseEventHandler::Register(); } @@ -461,24 +458,6 @@ void Skylighting::SetViewFrustum::thunk(RE::NiCamera* a_camera, RE::NiFrustum* a func(a_camera, a_frustum); } -void Skylighting::SetViewFrustumVR::thunk(RE::NiCamera* a_camera, RE::NiFrustum* a_frustum, uint a_eyeIndex) -{ - auto& skylighting = globals::features::skylighting; - - if (skylighting.inOcclusion) { - uint corner = skylighting.frameCount % 4; - - float frustumSize = a_frustum->fTop; - - a_frustum->fBottom = (corner == 0 || corner == 1) ? -frustumSize : 0.0f; - a_frustum->fLeft = (corner == 0 || corner == 2) ? -frustumSize : 0.0f; - a_frustum->fRight = (corner == 1 || corner == 3) ? frustumSize : 0.0f; - a_frustum->fTop = (corner == 2 || corner == 3) ? frustumSize : 0.0f; - } - - func(a_camera, a_frustum, a_eyeIndex); -} - void Skylighting::RenderOcclusion() { ZoneScopedS(8); diff --git a/src/Features/Skylighting.h b/src/Features/Skylighting.h index 900f709ab2..78aa3ee0c6 100644 --- a/src/Features/Skylighting.h +++ b/src/Features/Skylighting.h @@ -6,7 +6,6 @@ struct Skylighting : Feature static constexpr std::string_view MOD_ID = "139352"; public: - virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Skylighting"; } virtual std::string GetDisplayName() override { return T("feature.skylighting.name", "Skylighting"); } @@ -114,12 +113,6 @@ struct Skylighting : Feature static inline REL::Relocation func; }; - struct SetViewFrustumVR - { - static void thunk(RE::NiCamera* a_camera, RE::NiFrustum* a_frustum, uint a_eyeIndex); - static inline REL::Relocation func; - }; - // Event handler class MenuOpenCloseEventHandler : public RE::BSTEventSink { diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index 591d1ae194..6e4136d425 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -230,7 +230,7 @@ void SubsurfaceScattering::DrawSSS() auto dispatchCount = Util::GetScreenDispatchCount(); { - auto cameraData = Util::GetCameraData(0); + auto cameraData = globals::game::shadowState->GetRuntimeData().cameraData.getEye(); blurCBData.SSSS_FOVY = atan(1.0f / cameraData.projMat.m[0][0]) * 2.0f * (180.0f / 3.14159265359f); diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index 7ebd3ae33e..edcc3954d2 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -135,5 +135,4 @@ struct SubsurfaceScattering : Feature } }; - virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index b298fdbb13..b5e467eeac 100644 --- a/src/Features/TerrainBlending.cpp +++ b/src/Features/TerrainBlending.cpp @@ -6,7 +6,6 @@ #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" -#include "VR.h" #define I18N_KEY_PREFIX "feature.terrain_blending." @@ -44,7 +43,7 @@ namespace // 1) PS slot 17 override: bind TB-selected depth SRV for OBB depth reads; prevents occlusion instability / mesh popping. // 2) PS slot 2 override: bind TB-selected depth SRV for shadowmask reads; prevents unstable/moving ground shadow imprint, and dark overlay style artifacts. // 3) OM depth override: force DepthFunc=ALWAYS only on descriptor 0x1062002; mitigate shadowmask ground artifacts caused by failed depth testing in 0x1062002. - // All override paths below are gated by IsEngineHookFeatureGateSatisfied and all are VR-specific at runtime (isVR, gateSatisfied). + // All override paths below are gated by IsEngineHookFeatureGateSatisfied. // Developer Mode only: logs one hook snapshot per session ([TB Override]/[TB DepthOverride]) and explicit fallback activate/reset events. // Fallbacks: caller fallback is in ShouldAllowCallerWithFallback(...) (2 and 3 widen after 5 rejects and collapse on first allowlisted hit), SRV-source fallback is in Util::GetCurrentSceneDepthSRV(...). // Pixel descriptors: @@ -53,27 +52,9 @@ namespace constexpr uint32_t kShadowmaskDepthDescriptor0 = 0x262002u; constexpr uint32_t kShadowmaskDepthDescriptor1 = 0x1062002u; - // Module RVAs from _ReturnAddress() at hooked engine callsites. - // Ownership: - // - Shared slot2 + depth-override callers: 0x1351AD4, 0xDBDD68 - // - Depth-override-only caller: 0x1349B7F - const uint32_t kCallerRvaSlot2AndDepthOverrideA = static_cast(REL::Relocate(0u, 0u, 0x1351AD4u)); - const uint32_t kCallerRvaSlot2AndDepthOverrideB = static_cast(REL::Relocate(0u, 0u, 0xDBDD68u)); - const uint32_t kCallerRvaDepthOverrideOnly = static_cast(REL::Relocate(0u, 0u, 0x1349B7Fu)); - - // Slot2 rewrite allowlist (PS slot 2 = shadowmask depth SRV override path). - // Includes only callsites validated for shadowmask slot2 rebinding. - const std::array kSlot2CallerAllowlistRvas = { - kCallerRvaSlot2AndDepthOverrideA, - kCallerRvaSlot2AndDepthOverrideB - }; - // Descriptor-scoped OM depth override allowlist (0x1062002 only). - // Contains the two shared callers above plus one depth-override-only caller. - const std::array kDepthOverrideCallerAllowlistRvas = { - kCallerRvaSlot2AndDepthOverrideB, - kCallerRvaSlot2AndDepthOverrideA, - kCallerRvaDepthOverrideOnly - }; + // Allowlists of module RVAs for _ReturnAddress()-based hook dispatch. + const std::array kSlot2CallerAllowlistRvas = {}; + const std::array kDepthOverrideCallerAllowlistRvas = {}; constexpr bool kEnableAutoBroadSlot2Fallback = true; constexpr uint64_t kSlot2AutoFallbackRejectThreshold = 5; constexpr bool kEnableAutoBroadDepthOverrideFallback = true; @@ -118,8 +99,7 @@ namespace bool ShouldUseBlendedDepthSRV() { - auto& vr = globals::features::vr; - return !globals::game::isVR || !vr.gDepthBufferCulling || !*vr.gDepthBufferCulling; + return true; } bool IsShadowmaskDepthDescriptorWhitelisted(const uint32_t a_descriptor) @@ -228,13 +208,9 @@ namespace a_callerRva); } - bool IsEngineHookFeatureGateSatisfied(const TerrainBlending& a_singleton) + bool IsEngineHookFeatureGateSatisfied([[maybe_unused]] const TerrainBlending& a_singleton) { - if (!globals::game::isVR || !a_singleton.loaded || !a_singleton.settings.Enabled) { - return false; - } - - return !ShouldUseBlendedDepthSRV(); + return false; } struct EngineHookPassGateState @@ -751,7 +727,7 @@ void TerrainBlending::TerrainShaderHacks() auto dsv = terrainDepth.views[0]; context->OMSetRenderTargets(0, nullptr, dsv); auto shadowState = globals::game::shadowState; - GET_INSTANCE_MEMBER(currentVertexShader, shadowState) + auto& currentVertexShader = shadowState->GetRuntimeData().currentVertexShader; context->VSSetShader((ID3D11VertexShader*)currentVertexShader->shader, NULL, NULL); } renderAltTerrain = !renderAltTerrain; @@ -826,7 +802,7 @@ void TerrainBlending::BlendPrepassDepths() auto stateUpdateFlags = globals::game::stateUpdateFlags; stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); // CopyResource(terrainDepth <- mainDepth) eliminated: main depth is now written - // directly into mainDepthCopy (u2) by the CS above, saving a full-stereo D24S8 copy. + // directly into mainDepthCopy (u2) by the CS above, saving a full D24S8 copy. if (globals::state->frameAnnotations) globals::state->EndPerfEvent(); @@ -861,7 +837,7 @@ void TerrainBlending::Hooks::Main_RenderDepth::thunk(bool a1, bool a2) globals::game::graphicsState->SetCameraData(RE::Main::WorldRootCamera(), 1); - singleton.averageEyePosition = Util::GetAverageEyePosition(); + singleton.eyePosition = Util::GetEyePosition(); const bool tbActive = shaderCache->IsEnabled() && singleton.settings.Enabled; const bool useBlendedDepthSRV = tbActive && ShouldUseBlendedDepthSRV(); @@ -913,7 +889,7 @@ void TerrainBlending::Hooks::BSBatchRenderer__RenderPassImmediately::thunk(RE::B bool inTerrain = a_pass->shaderProperty && a_pass->shaderProperty->flags.all(RE::BSShaderProperty::EShaderPropertyFlag::kMultiTextureLandscape); if (inTerrain && a_pass->geometry) { - if ((a_pass->geometry->worldBound.center.GetDistance(singleton.averageEyePosition) - a_pass->geometry->worldBound.radius) > 1024.0f) { + if ((a_pass->geometry->worldBound.center.GetDistance(singleton.eyePosition) - a_pass->geometry->worldBound.radius) > 1024.0f) { inTerrain = false; } } @@ -1025,9 +1001,9 @@ void TerrainBlending::RenderTerrainBlendingPasses() if (globals::state->frameAnnotations) globals::state->BeginPerfEvent("Terrain Blending - Render Passes"); - GET_INSTANCE_MEMBER(alphaBlendMode, shadowState) - GET_INSTANCE_MEMBER(alphaBlendWriteMode, shadowState) - GET_INSTANCE_MEMBER(depthStencilDepthMode, shadowState) + auto& alphaBlendMode = shadowState->GetRuntimeData().alphaBlendMode; + auto& alphaBlendWriteMode = shadowState->GetRuntimeData().alphaBlendWriteMode; + auto& depthStencilDepthMode = shadowState->GetRuntimeData().depthStencilDepthMode; // Reset alpha write and enable alpha blending alphaBlendWriteMode = 1; diff --git a/src/Features/TerrainBlending.h b/src/Features/TerrainBlending.h index 8f13612eb9..04f8c6bb06 100644 --- a/src/Features/TerrainBlending.h +++ b/src/Features/TerrainBlending.h @@ -23,7 +23,6 @@ struct TerrainBlending : Feature }; virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } - virtual bool SupportsVR() override { return true; } struct Settings { @@ -55,7 +54,7 @@ struct TerrainBlending : Feature bool renderTerrainDepth = false; bool renderAltTerrain = false; - RE::NiPoint3 averageEyePosition; + RE::NiPoint3 eyePosition; struct RenderPass { @@ -150,7 +149,7 @@ struct TerrainBlending : Feature static void Install() { // To know when we are rendering z-prepass depth vs shadows depth - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x395, 0x395, 0x2EE)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x395, 0x395)); // To know when shadowmask phase ends (for releasing engine hook overrides) stl::detour_thunk(REL::RelocationID(100422, 107140)); diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index bcd09054e9..6bef135c62 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -228,8 +228,7 @@ void TerrainHelper::Load() { // Install TESObjectLAND hook early so TH is inner relative to TruePBR's PostPostLoad hook. // This ensures TH reads the vanilla material hashKey before TruePBR replaces it with a PBR material. - // This intentionally matches TruePBR's REL::RelocationID(18368, 18791), so no extra - // VR gate is needed unless those offsets diverge. + // This intentionally matches TruePBR's REL::RelocationID(18368, 18791). logger::info("[Terrain Helper] Hooking TESObjectLAND"); stl::detour_thunk(REL::RelocationID(18368, 18791)); } diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index c477a27ce0..796c1628da 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -39,7 +39,6 @@ struct TerrainHelper : Feature virtual void Load() override; virtual void DataLoaded() override; virtual void PostPostLoad() override; - virtual bool SupportsVR() override { return true; }; virtual std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } void SetShaderResources(ID3D11DeviceContext* a_context); diff --git a/src/Features/TerrainShadows.h b/src/Features/TerrainShadows.h index 3d824de82c..dd79a56f23 100644 --- a/src/Features/TerrainShadows.h +++ b/src/Features/TerrainShadows.h @@ -91,6 +91,5 @@ struct TerrainShadows : public Feature virtual inline void RestoreDefaultSettings() override { settings = {}; } virtual void ClearShaderCache() override; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; \ No newline at end of file diff --git a/src/Features/TerrainVariation.h b/src/Features/TerrainVariation.h index 83bf3385f1..66dc538276 100644 --- a/src/Features/TerrainVariation.h +++ b/src/Features/TerrainVariation.h @@ -16,7 +16,6 @@ struct TerrainVariation : Feature return (shaderType == RE::BSShader::Type::Lighting); } virtual bool IsCore() const override { return false; }; - virtual bool SupportsVR() override { return true; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index d3121345be..9e719a4cd3 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -350,7 +350,7 @@ void UnifiedWater::PostPostLoad() stl::detour_thunk(REL::RelocationID(13170, 13315)); stl::detour_thunk(REL::RelocationID(20029, 20463)); - stl::write_thunk_call(REL::RelocationID(31388, 32179).address() + REL::Relocate(0x360, 0x3BC, 0x35B)); + stl::write_thunk_call(REL::RelocationID(31388, 32179).address() + REL::Relocate(0x360, 0x3BC)); stl::write_vfunc<0x4, BSWaterShaderMaterial_ComputeCRC32>(RE::VTABLE_BSWaterShaderMaterial[0]); stl::detour_thunk(REL::RelocationID(30934, 31737)); diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 0de2594480..9ae1f05259 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -106,7 +106,6 @@ struct UnifiedWater : OverlayFeature virtual bool IsCore() const override { return true; } virtual bool IsDisabledByDefault() const override { return true; } - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 649b1af626..75c53d51c7 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -84,7 +84,7 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( pSwapChainDesc->BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; } - bool shouldProxy = !globals::game::isVR; + bool shouldProxy = true; if (shouldProxy) if (!pSwapChainDesc->Windowed) shouldProxy = false; @@ -218,9 +218,9 @@ void Upscaling::DrawSettings() // Check the current upscale method auto upscaleMethod = GetUpscaleMethod(); - // Display warning for DLSS resolution limits (non-VR only; VR handles this automatically) - if (!globals::game::isVR && upscaleMethod == UpscaleMethod::kDLSS) { - auto screenSize = globals::state->screenSize; + // Display warning for DLSS resolution limits + if (upscaleMethod == UpscaleMethod::kDLSS) { + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; if (screenSize.x > streamline.MAX_RESOLUTION || screenSize.y > streamline.MAX_RESOLUTION) { Util::Text::Warning("Warning: Requested resolution %.0f x %.0f exceeds maximum supported resolution %d x %d for DLSS.", screenSize.x, screenSize.y, streamline.MAX_RESOLUTION, streamline.MAX_RESOLUTION); @@ -293,73 +293,71 @@ void Upscaling::DrawSettings() const bool frameGenerationDx12PathActive = IsFrameGenerationDx12PathActive(); - if (!globals::game::isVR) { - if (ImGui::TreeNodeEx(T(TKEY("frame_generation"), "Frame Generation"), ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("%s", T(TKEY("frame_generation_desc"), - "Frame Generation interpolates real frames with generated ones for a smoother experience")); - ImGui::Text("%s", T(TKEY("frame_generation_tech"), - "Uses AMD FSR Frame Generation technology")); - if (HasFrameGenModule()) - ImGui::Text("%s", T(TKEY("frame_generation_available"), - "AMD FSR Frame Generation is available.")); - ImGui::Text("%s", T(TKEY("frame_generation_proxy_note"), - "Requires a D3D11 to D3D12 proxy which can create compatibility issues")); - ImGui::Text("%s", T(TKEY("frame_generation_restart_note"), - "Toggling this setting requires a restart to work correctly")); - - bool onlyRequiresRestart = true; - - if (!isWindowed) { - Util::Text::Warning("Warning: Requires windowed mode"); - - onlyRequiresRestart = false; - } + if (ImGui::TreeNodeEx(T(TKEY("frame_generation"), "Frame Generation"), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%s", T(TKEY("frame_generation_desc"), + "Frame Generation interpolates real frames with generated ones for a smoother experience")); + ImGui::Text("%s", T(TKEY("frame_generation_tech"), + "Uses AMD FSR Frame Generation technology")); + if (HasFrameGenModule()) + ImGui::Text("%s", T(TKEY("frame_generation_available"), + "AMD FSR Frame Generation is available.")); + ImGui::Text("%s", T(TKEY("frame_generation_proxy_note"), + "Requires a D3D11 to D3D12 proxy which can create compatibility issues")); + ImGui::Text("%s", T(TKEY("frame_generation_restart_note"), + "Toggling this setting requires a restart to work correctly")); + + bool onlyRequiresRestart = true; + + if (!isWindowed) { + Util::Text::Warning("Warning: Requires windowed mode"); + + onlyRequiresRestart = false; + } - if (lowRefreshRate && !settings.frameGenerationForceEnable) { - Util::Text::Warning("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); + if (lowRefreshRate && !settings.frameGenerationForceEnable) { + Util::Text::Warning("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); - onlyRequiresRestart = false; - } + onlyRequiresRestart = false; + } - if (fidelityFXMissing) { - Util::Text::Warning("Warning: FidelityFX DLLs are not loaded"); + if (fidelityFXMissing) { + Util::Text::Warning("Warning: FidelityFX DLLs are not loaded"); - onlyRequiresRestart = false; - } + onlyRequiresRestart = false; + } - if (onlyRequiresRestart && settings.frameGenerationMode && !frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); + if (onlyRequiresRestart && settings.frameGenerationMode && !frameGenerationDx12PathActive) + Util::Text::Warning("Warning: Requires restart"); - if (!settings.frameGenerationMode && frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); + if (!settings.frameGenerationMode && frameGenerationDx12PathActive) + Util::Text::Warning("Warning: Requires restart"); - bool fgEnabled = settings.frameGenerationMode != 0; - if (ImGui::Checkbox(T(TKEY("frame_generation"), "Frame Generation"), &fgEnabled)) - settings.frameGenerationMode = fgEnabled ? 1 : 0; + bool fgEnabled = settings.frameGenerationMode != 0; + if (ImGui::Checkbox(T(TKEY("frame_generation"), "Frame Generation"), &fgEnabled)) + settings.frameGenerationMode = fgEnabled ? 1 : 0; - if (!frameGenerationDx12PathActive) - ImGui::BeginDisabled(); + if (!frameGenerationDx12PathActive) + ImGui::BeginDisabled(); - bool flEnabled = settings.frameLimitMode != 0; - if (ImGui::Checkbox(T(TKEY("frame_limit_vrr"), "Frame Limit (Variable Refresh Rate)"), &flEnabled)) - settings.frameLimitMode = flEnabled ? 1 : 0; + bool flEnabled = settings.frameLimitMode != 0; + if (ImGui::Checkbox(T(TKEY("frame_limit_vrr"), "Frame Limit (Variable Refresh Rate)"), &flEnabled)) + settings.frameLimitMode = flEnabled ? 1 : 0; - if (!frameGenerationDx12PathActive) - ImGui::EndDisabled(); + if (!frameGenerationDx12PathActive) + ImGui::EndDisabled(); - ImGui::TextWrapped("Allows frame generation to function on low refresh rate monitors. Detected: %.2f Hz", refreshRate); - bool fgForce = settings.frameGenerationForceEnable != 0; - if (ImGui::Checkbox(T(TKEY("force_enable_frame_generation"), "Force Enable Frame Generation"), &fgForce)) - settings.frameGenerationForceEnable = fgForce ? 1 : 0; + ImGui::TextWrapped("Allows frame generation to function on low refresh rate monitors. Detected: %.2f Hz", refreshRate); + bool fgForce = settings.frameGenerationForceEnable != 0; + if (ImGui::Checkbox(T(TKEY("force_enable_frame_generation"), "Force Enable Frame Generation"), &fgForce)) + settings.frameGenerationForceEnable = fgForce ? 1 : 0; - ImGui::Checkbox(T(TKEY("frame_generation_in_menus"), "Frame Generation in Menus"), &settings.frameGenerationAllowInMenus); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_1"), "Keeps frame generation active while game menus are open.")); - ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_2"), "May feel smoother, but increases menu input latency.")); - } - - ImGui::TreePop(); + ImGui::Checkbox(T(TKEY("frame_generation_in_menus"), "Frame Generation in Menus"), &settings.frameGenerationAllowInMenus); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_1"), "Keeps frame generation active while game menus are open.")); + ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_2"), "May feel smoother, but increases menu input latency.")); } + + ImGui::TreePop(); } if (streamline.reflexSupportedOnCurrentAdapter && ImGui::TreeNodeEx(T(TKEY("nvidia_reflex"), "NVIDIA Reflex"), ImGuiTreeNodeFlags_DefaultOpen)) { @@ -451,68 +449,6 @@ void Upscaling::DrawSettings() ImGui::Text("%s", T(TKEY("streamline_logging_tooltip"), "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.")); } - // VR Debug visualization -- per-eye buffers and native inputs - if (globals::game::isVR) { - ImGui::Separator(); - static float debugRescale = 0.15f; - ImGui::SliderFloat(T(TKEY("view_resize"), "View Resize"), &debugRescale, 0.05f, 1.f); - - if (ImGui::TreeNode(T(TKEY("upscaling_intermediates"), "Upscaling Intermediates"))) { - if (vrIntermediateMotionVectors[0]) { - bool isDLSS = GetUpscaleMethod() == UpscaleMethod::kDLSS; - if (vrIntermediateColorIn[0] && vrIntermediateColorOut[0]) { - BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[0], "Left Eye In", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[1], "Right Eye In", debugRescale) - if (!isDLSS) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[0], "Left Eye Out", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[1], "Right Eye Out", debugRescale) - } - BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[0], "Left Eye MVec", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[1], "Right Eye MVec", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[0], "Left Eye Reactive", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[1], "Right Eye Reactive", debugRescale) - if (vrIntermediateTransparencyMask[0]) { - BUFFER_VIEWER_NODE_TITLE(vrIntermediateTransparencyMask[0], "Left Eye Transparency", debugRescale) - BUFFER_VIEWER_NODE_TITLE(vrIntermediateTransparencyMask[1], "Right Eye Transparency", debugRescale) - } - } else { - ImGui::TextDisabled("%s", T(TKEY("vr_intermediates_not_created"), "VR intermediates not yet created (enter game world)")); - } - ImGui::TreePop(); - } - - if (ImGui::TreeNode(T(TKEY("native_inputs"), "Native Inputs"))) { - auto renderer = globals::game::renderer; - auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - auto& mvec = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - - auto DisplayRT = [&](const char* label, ID3D11Texture2D* tex, ID3D11ShaderResourceView* srv) { - if (srv && tex) { - D3D11_TEXTURE2D_DESC desc; - tex->GetDesc(&desc); - char buf[128]; - snprintf(buf, sizeof(buf), "%s (%ux%u)", label, desc.Width, desc.Height); - if (ImGui::TreeNode(buf)) { - ImGui::Image(srv, { desc.Width * debugRescale, desc.Height * debugRescale }); - ImGui::TreePop(); - } - } - }; - - DisplayRT("kMAIN (Color Input)", (ID3D11Texture2D*)main.texture, (ID3D11ShaderResourceView*)main.SRV); - DisplayRT("Motion Vectors", (ID3D11Texture2D*)mvec.texture, (ID3D11ShaderResourceView*)mvec.SRV); - DisplayRT("Depth", depth.texture, depth.depthSRV); - - if (reactiveMaskTexture) - BUFFER_VIEWER_NODE_TITLE(reactiveMaskTexture, "Reactive Mask", debugRescale) - if (transparencyCompositionMaskTexture) - BUFFER_VIEWER_NODE_TITLE(transparencyCompositionMaskTexture, "Transparency Mask", debugRescale) - - ImGui::TreePop(); - } - } - ImGui::Separator(); Util::DrawDllVersionTable("AMD FidelityFX DLLs (click to open folder)", FidelityFX::PluginDir, FidelityFX::dllVersions, "ffx_dll_versions"); Util::DrawDllVersionTable("NVIDIA Streamline DLLs (click to open folder)", Streamline::PluginDir, Streamline::dllVersions, "sl_dll_versions"); @@ -589,31 +525,6 @@ void Upscaling::DataLoaded() static auto fDRClampOffset = RE::GetINISetting("fDRClampOffset:Display"); fDRClampOffset->data.f = 0.0f; - // VR + DLSS workaround: rebuild the DLSS feature on cell/worldspace transitions to - // clear a persistent post-load GPU-time regression (see pendingDLSSReset comment). - if (globals::game::isVR) - MenuOpenCloseEventHandler::Register(); -} - -RE::BSEventNotifyControl Upscaling::MenuOpenCloseEventHandler::ProcessEvent( - const RE::MenuOpenCloseEvent* a_event, RE::BSTEventSource*) -{ - if (a_event && a_event->menuName == RE::LoadingMenu::MENU_NAME && !a_event->opening) - globals::features::upscaling.pendingDLSSReset.store(true, std::memory_order_relaxed); - return RE::BSEventNotifyControl::kContinue; -} - -bool Upscaling::MenuOpenCloseEventHandler::Register() -{ - static MenuOpenCloseEventHandler singleton; - auto ui = globals::game::ui; - if (!ui) { - logger::error("[Upscaling] UI event source not found; DLSS reset-on-load disabled"); - return false; - } - ui->GetEventSource()->AddEventSink(&singleton); - logger::info("[Upscaling] Registered MenuOpenCloseEventHandler for DLSS reset-on-load"); - return true; } void Upscaling::Load() @@ -639,24 +550,22 @@ void Upscaling::PostPostLoad() stl::detour_thunk(REL::RelocationID(79947, 82084)); // Calculates resolution and jitter - stl::write_thunk_call(REL::RelocationID(75460, 77245).address() + REL::Relocate(0xE5, isGOG ? 0x133 : 0xE2, 0x104)); + stl::write_thunk_call(REL::RelocationID(75460, 77245).address() + REL::Relocate(0xE5, isGOG ? 0x133 : 0xE2)); // Disables the original dynamic resolution system - REL::safe_write(REL::RelocationID(35556, 36555).address() + REL::Relocate(0x2D, 0x2D, 0x25), REL::NOP5, sizeof(REL::NOP5)); + REL::safe_write(REL::RelocationID(35556, 36555).address() + REL::Relocate(0x2D, 0x2D), REL::NOP5, sizeof(REL::NOP5)); // Performs upscaling in between volumetric lighting and post processing - stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7, 0x206)); + stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7)); // Patches RSSetScissorRect calls to use dynamic resolution - // This is a PC-specific function hence it was missing - if (!globals::game::isVR) - stl::detour_thunk(REL::RelocationID(75564, 77365)); + stl::detour_thunk(REL::RelocationID(75564, 77365)); // Patches facegen texture generation to not use dynamic resolution stl::detour_thunk(REL::RelocationID(26455, 27041)); // Patches precipitation camera to not use dynamic resolution - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1)); // Forces FXAA off stl::detour_thunk(REL::RelocationID(98974, 105626)); @@ -817,18 +726,6 @@ void Upscaling::CheckResources(UpscaleMethod a_upscalemethod) streamline.DestroyDLSSResources(); else if (previousUpscaleMode == UpscaleMethod::kFSR) fidelityFX.DestroyFSRResources(); - - if (globals::game::isVR) { - for (int i = 0; i < 2; i++) { - vrIntermediateColorIn[i].reset(); - vrIntermediateColorOut[i].reset(); - vrIntermediateLinearDepth[i].reset(); - vrIntermediateMotionVectors[i].reset(); - vrIntermediateReactiveMask[i].reset(); - vrIntermediateTransparencyMask[i].reset(); - } - vrIntermediateDepth.reset(); - } } if (a_upscalemethod == UpscaleMethod::kFSR) fidelityFX.CreateFSRResources(); @@ -851,20 +748,6 @@ ID3D11ComputeShader* Upscaling::GetEncodeTexturesCS() auto upscaleMethod = GetUpscaleMethod(); uint methodIndex = (uint)upscaleMethod; - // VR FSR needs a separate variant: DEPTH_OUTPUT converts the R24G8_TYPELESS game depth to - // R32_FLOAT so GetFfxResourceDescriptionDX11() returns a valid format instead of UNKNOWN. - if (globals::game::isVR && upscaleMethod == UpscaleMethod::kFSR) { - if (!encodeTexturesCSDepthOutput) { - logger::debug("Compiling EncodeTexturesCS.hlsl for VR FSR (FSR + DEPTH_OUTPUT)"); - std::vector> defines = { - { "FSR", "" }, - { "DEPTH_OUTPUT", "" } - }; - encodeTexturesCSDepthOutput.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/EncodeTexturesCS.hlsl", defines, "cs_5_0")); - } - return encodeTexturesCSDepthOutput.get(); - } - if (!encodeTexturesCS[methodIndex]) { logger::debug("Compiling EncodeTexturesCS.hlsl for upscale method {}", methodIndex); @@ -904,8 +787,6 @@ ID3D11PixelShader* Upscaling::GetUnderwaterMaskUpscalePS() if (!underwaterMaskUpscalePS) { logger::debug("Compiling UnderwaterMaskPS.hlsl"); std::vector> defines = { { "PSHADER", "" } }; - if (globals::game::isVR) - defines.push_back({ "VR", "" }); underwaterMaskUpscalePS.attach((ID3D11PixelShader*)Util::CompileShader(L"Data/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl", defines, "ps_5_0")); } @@ -962,236 +843,6 @@ eastl::unique_ptr Upscaling::CreateTextureFromSource(ID3D11Resource* return tex; } -void Upscaling::CreateVRIntermediateTextures(uint32_t inWidth, uint32_t inHeight, uint32_t outWidth, uint32_t outHeight, - ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc) -{ - // Right-eye-only depth intermediate for DLSS. Streamline.Upscale copies the right-eye depth - // slice here before evaluating DLSS eye 1; eye 0 reads the combined stereo depth directly at - // zero offset. R24G8_TYPELESS matches the game's D24S8_TYPELESS cast group — R32_TYPELESS is - // a different cast group and produces silent zero-copy failures. - { - D3D11_TEXTURE2D_DESC depthDesc = {}; - depthDesc.Width = inWidth; - depthDesc.Height = inHeight; - depthDesc.MipLevels = 1; - depthDesc.ArraySize = 1; - depthDesc.Format = DXGI_FORMAT_R24G8_TYPELESS; - depthDesc.SampleDesc.Count = 1; - depthDesc.Usage = D3D11_USAGE_DEFAULT; - depthDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - vrIntermediateDepth = eastl::make_unique(depthDesc); - - Util::SetResourceName(vrIntermediateDepth->resource.get(), "Upscale_Depth_Right"); - - D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; - srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS; - srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; - srvDesc.Texture2D.MipLevels = 1; - vrIntermediateDepth->CreateSRV(srvDesc); - } - - // All buffers are per-eye: Streamline validates all extents against the input color texture - // dimensions, so every tagged resource must be isolated per-eye at {0,0}. - for (int i = 0; i < 2; i++) { - std::string suffix = (i == 0) ? "Left" : "Right"; - - vrIntermediateColorIn[i] = CreateTextureFromSource(colorSrc, inWidth, inHeight, false, true, true, ("Upscale_ColorIn_" + suffix).c_str()); - vrIntermediateColorOut[i] = CreateTextureFromSource(colorSrc, outWidth, outHeight, false, true, false, ("Upscale_ColorOut_" + suffix).c_str()); - - // Linear depth: R32_FLOAT so FSR's GetFfxResourceDescriptionDX11() returns a valid format. - // EncodeTexturesCS writes the non-linear depth as R32_FLOAT for FSR. Kept separate from - // vrIntermediateDepth (R24G8_TYPELESS) which Streamline copies into for DLSS right eye. - { - D3D11_TEXTURE2D_DESC ldDesc = {}; - ldDesc.Width = inWidth; - ldDesc.Height = inHeight; - ldDesc.MipLevels = 1; - ldDesc.ArraySize = 1; - ldDesc.Format = DXGI_FORMAT_R32_FLOAT; - ldDesc.SampleDesc.Count = 1; - ldDesc.Usage = D3D11_USAGE_DEFAULT; - ldDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; - vrIntermediateLinearDepth[i] = eastl::make_unique(ldDesc); - - Util::SetResourceName(vrIntermediateLinearDepth[i]->resource.get(), ("Upscale_LinearDepth_" + suffix).c_str()); - - D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc2 = {}; - srvDesc2.Format = DXGI_FORMAT_R32_FLOAT; - srvDesc2.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; - srvDesc2.Texture2D.MipLevels = 1; - vrIntermediateLinearDepth[i]->CreateSRV(srvDesc2); - - D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc2 = {}; - uavDesc2.Format = DXGI_FORMAT_R32_FLOAT; - uavDesc2.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D; - uavDesc2.Texture2D.MipSlice = 0; - vrIntermediateLinearDepth[i]->CreateUAV(uavDesc2); - } - - // UAV required: EncodeTexturesCS writes directly into these per-eye buffers - vrIntermediateMotionVectors[i] = CreateTextureFromSource(mvecSrc, inWidth, inHeight, false, true, true, ("Upscale_MVec_" + suffix).c_str()); - vrIntermediateReactiveMask[i] = CreateTextureFromSource(reactiveSrc, inWidth, inHeight, false, true, true, ("Upscale_Reactive_" + suffix).c_str()); - vrIntermediateTransparencyMask[i] = CreateTextureFromSource(transparencySrc, inWidth, inHeight, false, true, true, ("Upscale_Transparency_" + suffix).c_str()); - } - - logger::info("[Upscaling] Created VR intermediate textures: per-eye in {}x{}, out {}x{}", - inWidth, inHeight, outWidth, outHeight); -} - -void Upscaling::EnsureVRIntermediateTextures() -{ - auto renderer = globals::game::renderer; - auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - auto& motionVectorRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - - auto screenSize = globals::state->screenSize; - auto renderSize = Util::ConvertToDynamic(screenSize); - - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; - uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); - uint32_t eyeHeightIn = (uint32_t)renderSize.y; - - bool needsRecreate = !vrIntermediateColorIn[0] || !vrIntermediateColorOut[0] || !vrIntermediateLinearDepth[0]; - if (!needsRecreate) { - needsRecreate = (vrIntermediateColorIn[0]->desc.Width != eyeWidthIn || - vrIntermediateColorIn[0]->desc.Height != eyeHeightIn || - vrIntermediateColorOut[0]->desc.Width != eyeWidthOut || - vrIntermediateColorOut[0]->desc.Height != eyeHeightOut); - } - if (needsRecreate) { - logger::info("[Upscaling] (Re)creating VR intermediates: per-eye in {}x{}, out {}x{}", - eyeWidthIn, eyeHeightIn, eyeWidthOut, eyeHeightOut); - CreateVRIntermediateTextures(eyeWidthIn, eyeHeightIn, eyeWidthOut, eyeHeightOut, - main.texture, motionVectorRT.texture, - reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get()); - } -} - -void Upscaling::PreparePerEyeInputs(ID3D11Resource* colorSrc) -{ - if (!globals::game::isVR) - return; - - auto state = globals::state; - if (state->frameAnnotations) - state->BeginPerfEvent("VR Upscaling Prepare"); - - auto context = globals::d3d::context; - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - - uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); - uint32_t eyeHeightIn = (uint32_t)renderSize.y; - - // Textures guaranteed to exist: EnsureVRIntermediateTextures() was called in Upscale() - // Read the original game depth SRV for ClearHMDMask — the combined stereo buffer is - // definitively valid here, whereas the per-eye copy may silently produce zeros on some - // depth-stencil format / driver combinations. - auto& depthTexture = globals::game::renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - auto& motionVectorRT = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - - for (uint32_t i = 0; i < 2; ++i) { - uint32_t offsetXIn = (i == 1) ? eyeWidthIn : 0; - D3D11_BOX srcBox = { offsetXIn, 0, 0, offsetXIn + eyeWidthIn, eyeHeightIn, 1 }; - - context->CopySubresourceRegion(vrIntermediateColorIn[i]->resource.get(), 0, 0, 0, 0, colorSrc, 0, &srcBox); - context->CopySubresourceRegion(vrIntermediateMotionVectors[i]->resource.get(), 0, 0, 0, 0, motionVectorRT.texture, 0, &srcBox); - - uint32_t depthOffset = (i == 1) ? eyeWidthIn : 0; - ClearHMDMask(vrIntermediateColorIn[i]->uav.get(), depthTexture.depthSRV, - eyeWidthIn, eyeHeightIn, depthOffset, 0); - } - - if (state->frameAnnotations) - state->EndPerfEvent(); -} - -void Upscaling::FinalizePerEyeOutputs(ID3D11Resource* colorDst) -{ - ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "VR Upscaling - Finalize Per Eye"); - - if (!globals::game::isVR) - return; - - auto state = globals::state; - if (state->frameAnnotations) - state->BeginPerfEvent("VR Upscaling Finalize"); - - auto context = globals::d3d::context; - auto screenSize = state->screenSize; - - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; - - // Write upscaled outputs back - for (uint32_t i = 0; i < 2; ++i) { - uint32_t offsetXOut = (i == 1) ? eyeWidthOut : 0; - D3D11_BOX outBox = { 0, 0, 0, eyeWidthOut, eyeHeightOut, 1 }; - context->CopySubresourceRegion(colorDst, 0, offsetXOut, 0, 0, vrIntermediateColorOut[i]->resource.get(), 0, &outBox); - } - - if (state->frameAnnotations) - state->EndPerfEvent(); -} - -void Upscaling::ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, - uint32_t eyeWidth, uint32_t eyeHeight, uint32_t depthOffsetX, uint32_t colorOffsetX) -{ - if (!globals::game::isVR) - return; - - auto context = globals::d3d::context; - - if (!vrClearHMDMaskCS) { - vrClearHMDMaskCS.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/ClearHMDMaskCS.hlsl", {}, "cs_5_0")); - - D3D11_BUFFER_DESC cbDesc = {}; - cbDesc.ByteWidth = 16; // 4 uints - cbDesc.Usage = D3D11_USAGE_DYNAMIC; - cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - DX::ThrowIfFailed(globals::d3d::device->CreateBuffer(&cbDesc, nullptr, vrClearHMDMaskCB.put())); - } - - if (vrClearHMDMaskCS) { - auto dispatchX = (eyeWidth + 7) / 8; - auto dispatchY = (eyeHeight + 7) / 8; - - context->CSSetShader(vrClearHMDMaskCS.get(), nullptr, 0); - - ID3D11ShaderResourceView* srvs[1] = { depthSRV }; - context->CSSetShaderResources(0, 1, srvs); - - ID3D11UnorderedAccessView* uavs[1] = { colorUAV }; - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - - D3D11_MAPPED_SUBRESOURCE mapped{}; - context->Map(vrClearHMDMaskCB.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); - - uint32_t offsets[4] = { depthOffsetX, colorOffsetX, 0, 0 }; - - memcpy(mapped.pData, offsets, sizeof(offsets)); - context->Unmap(vrClearHMDMaskCB.get(), 0); - - ID3D11Buffer* cbs[1] = { vrClearHMDMaskCB.get() }; - context->CSSetConstantBuffers(0, 1, cbs); - - globals::profiler->BeginPass("Upscaling::ClearHMDMask"); - context->Dispatch(dispatchX, dispatchY, 1); - globals::profiler->EndPass(); - - // Unbind - ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; - ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; - ID3D11Buffer* nullCB[1] = { nullptr }; - context->CSSetShaderResources(0, 1, nullSRV); - context->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); - context->CSSetConstantBuffers(0, 1, nullCB); - context->CSSetShader(nullptr, nullptr, 0); - } -} - int32_t GetJitterPhaseCount(int32_t renderWidth, int32_t displayWidth) { const float basePhaseCount = 8.0f; @@ -1227,7 +878,7 @@ void Upscaling::ConfigureTAA() auto upscaleMethod = GetUpscaleMethod(); auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); + auto& BSImagespaceShaderISTemporalAA = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA; // Force enable TAA if needed BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod != UpscaleMethod::kNONE; @@ -1246,7 +897,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) // Get full screen size auto state = globals::state; - auto screenSize = state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; auto screenWidth = static_cast(screenSize.x); auto screenHeight = static_cast(screenSize.y); @@ -1264,19 +915,13 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) GetJitterOffset(&jitter.x, &jitter.y, state->frameCount, phaseCount); - if (globals::game::isVR) - a_viewport->projectionPosScaleX = -jitter.x / renderWidth; - else - a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; + a_viewport->projectionPosScaleX = -2.0f * jitter.x / renderWidth; a_viewport->projectionPosScaleY = 2.0f * jitter.y / renderHeight; } else { resolutionScale = { 1.0f, 1.0f }; - if (globals::game::isVR) - jitter.x = -a_viewport->projectionPosScaleX * screenWidth; - else - jitter.x = -a_viewport->projectionPosScaleX * screenWidth / 2.0f; + jitter.x = -a_viewport->projectionPosScaleX * screenWidth / 2.0f; jitter.y = a_viewport->projectionPosScaleY * screenHeight / 2.0f; } @@ -1292,8 +937,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) dynamicResolutionHeightRatio = resolutionScale.y; // Disable dynamic resolution unless the game explicitly enables it - if (!globals::game::isVR) - runtimeData.dynamicResolutionLock = 1; + runtimeData.dynamicResolutionLock = 1; } void Upscaling::SetupResources() @@ -1322,25 +966,7 @@ void Upscaling::SetupResources() depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; // Write to all depth bits depthStencilDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; // Always pass depth test (write all depths) - if (globals::game::isVR) { - depthStencilDesc.StencilEnable = true; // Enable stencil testing - depthStencilDesc.StencilReadMask = 0xFF; // Read all stencil bits - depthStencilDesc.StencilWriteMask = 0xFF; // Write to all stencil bits - - // Configure front-facing stencil operations - depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; // Replace on stencil fail - depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; // Replace on depth fail - depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Replace on pass - depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; // Always pass stencil test - - // Configure back-facing stencil operations (same as front) - depthStencilDesc.BackFace.StencilFailOp = depthStencilDesc.FrontFace.StencilFailOp; - depthStencilDesc.BackFace.StencilDepthFailOp = depthStencilDesc.FrontFace.StencilDepthFailOp; - depthStencilDesc.BackFace.StencilPassOp = depthStencilDesc.FrontFace.StencilPassOp; - depthStencilDesc.BackFace.StencilFunc = depthStencilDesc.FrontFace.StencilFunc; - } else { - depthStencilDesc.StencilEnable = false; // Disable stencil testing - } + depthStencilDesc.StencilEnable = false; // Disable stencil testing DX::ThrowIfFailed(globals::d3d::device->CreateDepthStencilState(&depthStencilDesc, upscaleDepthStencilState.put())); @@ -1389,10 +1015,9 @@ void Upscaling::SetupResources() void Upscaling::ClearShaderCache() { - for (int i = 0; i < 5; ++i) { + for (int i = 0; i < 4; ++i) { encodeTexturesCS[i] = nullptr; // com_ptr automatically releases } - encodeTexturesCSDepthOutput = nullptr; depthRefractionUpscalePS = nullptr; // com_ptr automatically releases underwaterMaskUpscalePS = nullptr; // com_ptr automatically releases @@ -1415,7 +1040,7 @@ void Upscaling::CopySharedD3D12Resources() { // Set up viewport for fullscreen rendering - auto screenSize = globals::state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; D3D11_VIEWPORT viewport = {}; viewport.TopLeftX = 0.0f; @@ -1594,7 +1219,7 @@ double Upscaling::GetRefreshRate(HWND a_window) bool Upscaling::IsFrameGenerationDx12PathActive() const { - return d3d12SwapChainActive && !globals::game::isVR; + return d3d12SwapChainActive; } bool Upscaling::IsFrameGenerationActive() const @@ -1754,44 +1379,30 @@ void Upscaling::Upscale() auto& normals = renderer->GetRuntimeData().renderTargets[globals::deferred->forwardRenderTargets[2]]; auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - // VR: ensure per-eye intermediate textures exist before the dispatch writes into them - if (globals::game::isVR) - EnsureVRIntermediateTextures(); - - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - uint32_t numEyes = globals::game::isVR ? 2 : 1; - uint32_t eyeRenderWidth = (uint32_t)(renderSize.x / numEyes); - uint32_t eyeRenderHeight = (uint32_t)renderSize.y; + auto renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); + uint32_t renderWidth = (uint32_t)renderSize.x; + uint32_t renderHeight = (uint32_t)renderSize.y; - // Sources are the same combined stereo buffers for both VR and non-VR. - // The shader applies EyeOffsetX to sample the correct half. ID3D11ShaderResourceView* views[4] = { temporalAAMask.SRV, normals.SRV, motionVector.SRV, depth.depthSRV }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); context->CSSetShader(GetEncodeTexturesCS(), nullptr, 0); - for (uint32_t i = 0; i < numEyes; ++i) { - uint32_t offsetX = i * eyeRenderWidth; - - UpscalingDataCB upscalingData; - upscalingData.trueSamplingDim = float2((float)eyeRenderWidth, (float)eyeRenderHeight); - upscalingData.eyeOffsetX = offsetX; - upscalingDataCB->Update(upscalingData); - auto upscalingBuffer = upscalingDataCB->CB(); - context->CSSetConstantBuffers(0, 1, &upscalingBuffer); - - // u2 (MotionVectorOutput): DLSS only — 5x5 dilated MVec for ghosting reduction. - // u3 (DepthOutput): VR FSR only — converts R24G8_TYPELESS to R32_FLOAT so - // GetFfxResourceDescriptionDX11() returns a valid format. DLSS depth is copied in Streamline.cpp. - ID3D11UnorderedAccessView* uavs[4] = { - globals::game::isVR ? vrIntermediateReactiveMask[i]->uav.get() : reactiveMaskTexture->uav.get(), - globals::game::isVR ? vrIntermediateTransparencyMask[i]->uav.get() : transparencyCompositionMaskTexture->uav.get(), - (upscaleMethod == UpscaleMethod::kDLSS) ? (globals::game::isVR ? vrIntermediateMotionVectors[i]->uav.get() : motionVectorCopyTexture->uav.get()) : nullptr, - (upscaleMethod == UpscaleMethod::kFSR && globals::game::isVR) ? vrIntermediateLinearDepth[i]->uav.get() : nullptr - }; - context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); + UpscalingDataCB upscalingData; + upscalingData.trueSamplingDim = float2((float)renderWidth, (float)renderHeight); + upscalingDataCB->Update(upscalingData); + auto upscalingBuffer = upscalingDataCB->CB(); + context->CSSetConstantBuffers(0, 1, &upscalingBuffer); + + // u2 (MotionVectorOutput): DLSS only — 5x5 dilated MVec for ghosting reduction. + ID3D11UnorderedAccessView* uavs[4] = { + reactiveMaskTexture->uav.get(), + transparencyCompositionMaskTexture->uav.get(), + (upscaleMethod == UpscaleMethod::kDLSS) ? motionVectorCopyTexture->uav.get() : nullptr, + nullptr + }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); - context->Dispatch((eyeRenderWidth + 7) / 8, (eyeRenderHeight + 7) / 8, 1); - } + context->Dispatch((renderWidth + 7) / 8, (renderHeight + 7) / 8, 1); ID3D11ShaderResourceView* nullViews[4] = { nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(nullViews), nullViews); @@ -1815,14 +1426,6 @@ void Upscaling::Upscale() TracyD3D11Zone(globals::state->tracyCtx, "Upscaling Dispatch"); if (upscaleMethod == UpscaleMethod::kDLSS) { - // VR-only workaround: a worldspace/cell transition causes ~2-3ms persistent GPU-time - // regression in the DLSS feature that only clears on a manual mode/preset toggle. - // Mirror that toggle by tearing down the DLSS feature on LoadingMenu close — the next - // SetDLSSOptions/slEvaluateFeature call below recreates it with current per-eye extents. - if (globals::game::isVR && pendingDLSSReset.exchange(false, std::memory_order_relaxed)) { - logger::debug("[Upscaling] LoadingMenu close detected — rebuilding DLSS feature"); - streamline.DestroyDLSSResources(); - } streamline.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVectorCopyTexture->resource.get()); } else if (upscaleMethod == UpscaleMethod::kFSR) { fidelityFX.Upscale(main.texture, reactiveMaskTexture->resource.get(), transparencyCompositionMaskTexture->resource.get(), motionVector.texture, settings.sharpnessFSR); @@ -1871,7 +1474,7 @@ void Upscaling::UpscaleDepth() return; } - auto screenSize = state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; if (screenSize.x <= 0.0f || screenSize.y <= 0.0f) { return; } @@ -1887,10 +1490,6 @@ void Upscaling::UpscaleDepth() !underwaterMask.texture || !underwaterMask.textureCopy || !underwaterMask.SRVCopy || !underwaterMask.RTV) { return; } - if (globals::game::isVR && (!depthCopy.views[0] || !depthCopy.stencilSRV)) { - return; - } - auto* fullscreenVS = GetUpscaleVS(); auto* depthUpscalePS = GetDepthRefractionUpscalePS(); auto* underwaterMaskPS = GetUnderwaterMaskUpscalePS(); @@ -1968,11 +1567,6 @@ void Upscaling::UpscaleDepth() // Skip alias copies to reduce unnecessary copy churn. copyIfNonAliased(depthCopy.texture, depth.texture); - // Clear stencil to be 0xFF - if (globals::game::isVR) { - context->ClearDepthStencilView(depthCopy.views[0], D3D11_CLEAR_STENCIL, 1.0f, 0xFF); - } - // Set depth stencil state to write 0x00 context->OMSetDepthStencilState(upscaleDepthStencilState.get(), 0x00); @@ -1981,10 +1575,7 @@ void Upscaling::UpscaleDepth() ID3D11ShaderResourceView* srvs[] = { refractionNormals.SRVCopy, depthCopy.depthSRV, depthCopy.stencilSRV }; context->PSSetShaderResources(0, ARRAYSIZE(srvs), srvs); - // kSAO_CAMERAZ is at quarter-stereo resolution in VR; the full-stereo viewport would - // corrupt only the top-left quarter. The engine's ISSAOCameraZ pass populates it correctly. - ID3D11RenderTargetView* rtvs[] = { refractionNormals.RTV, - globals::game::isVR ? nullptr : saoCameraZ.RTV }; + ID3D11RenderTargetView* rtvs[] = { refractionNormals.RTV, saoCameraZ.RTV }; context->OMSetRenderTargets(2, rtvs, depth.views[0]); context->PSSetShader(depthUpscalePS, nullptr, 0); @@ -2004,8 +1595,7 @@ void Upscaling::UpscaleDepth() context->OMSetDepthStencilState(nullptr, 0x00); - // t0: vanilla mask copy, t1: original depth (for VR per-eye analytical mask). - // depthCopy still holds the original pre-upscale depth here (VR re-copy deferred). + // t0: vanilla mask copy, t1: original depth. ID3D11ShaderResourceView* srvs[] = { underwaterMask.SRVCopy, depthCopy.depthSRV }; context->PSSetShaderResources(0, ARRAYSIZE(srvs), srvs); @@ -2018,12 +1608,6 @@ void Upscaling::UpscaleDepth() globals::profiler->EndPass(); } - // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. - if (globals::game::isVR) { - TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Depth VR Propagate"); - copyIfNonAliased(depthCopy.texture, depth.texture); - } - ID3D11ShaderResourceView* nullPSResources[3] = { nullptr, nullptr, nullptr }; context->PSSetShaderResources(0, ARRAYSIZE(nullPSResources), nullPSResources); @@ -2096,7 +1680,7 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 upscaling.ApplySharpening(); auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); + auto& BSImagespaceShaderISTemporalAA = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA; BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod == UpscaleMethod::kTAA; diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 52f98ca8e1..91ba36dea9 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -26,7 +26,6 @@ struct Upscaling : Feature virtual std::string GetDisplayName() override { return T("feature.upscaling.name", "Upscaling"); } virtual inline std::string GetShortName() override { return "Upscaling"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - virtual inline bool SupportsVR() override { return true; } virtual inline bool IsCore() const override { return false; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kDisplay; } @@ -80,9 +79,8 @@ struct Upscaling : Feature struct UpscalingDataCB { - float2 trueSamplingDim; // per-eye render dim in VR, full render dim otherwise - uint eyeOffsetX; // X offset into stereo source buffers; 0 for non-VR / left eye - uint pad0; + float2 trueSamplingDim; + float2 pad0; }; ConstantBuffer* jitterCB = nullptr; @@ -128,8 +126,7 @@ struct Upscaling : Feature void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod); void DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod); - winrt::com_ptr encodeTexturesCS[5]; // One for each UpscaleMethod - winrt::com_ptr encodeTexturesCSDepthOutput; // FSR + VR: converts R24G8_TYPELESS depth to R32_FLOAT + winrt::com_ptr encodeTexturesCS[4]; // One for each UpscaleMethod (kNONE, kTAA, kFSR, kDLSS) ID3D11ComputeShader* GetEncodeTexturesCS(); winrt::com_ptr depthRefractionUpscalePS; @@ -145,43 +142,10 @@ struct Upscaling : Feature winrt::com_ptr upscaleBlendState; winrt::com_ptr upscaleRasterizerState; - // Shared VR HMD Mask Clearing - winrt::com_ptr vrClearHMDMaskCS; - winrt::com_ptr vrClearHMDMaskCB; - // Helper to dispatch mask clearing for a single eye region - void ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, - uint32_t eyeWidth, uint32_t eyeHeight, uint32_t depthOffsetX, uint32_t colorOffsetX); - - // Shared VR Per-Eye Intermediate Buffers - // Owned here so both Streamline (DLSS) and FidelityFX (FSR) can use them. - eastl::unique_ptr vrIntermediateColorIn[2]; // per-eye render resolution - eastl::unique_ptr vrIntermediateColorOut[2]; // per-eye output resolution - eastl::unique_ptr vrIntermediateDepth; // right-eye render resolution (R24G8_TYPELESS, DLSS only) - eastl::unique_ptr vrIntermediateLinearDepth[2]; // per-eye render resolution (R32_FLOAT, for FSR) - eastl::unique_ptr vrIntermediateMotionVectors[2]; // per-eye render resolution - eastl::unique_ptr vrIntermediateReactiveMask[2]; // per-eye render resolution - eastl::unique_ptr vrIntermediateTransparencyMask[2]; // per-eye render resolution - - // Helper to create/resize per-eye buffers matching source formats - void CreateVRIntermediateTextures(uint32_t inWidth, uint32_t inHeight, uint32_t outWidth, uint32_t outHeight, - ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); - // Helper: Create a Texture2D matching source format at a given size static eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, bool copyBindFlags = false, bool createSRV = false, bool createUAV = false, const char* name = nullptr); - // Shared Pipeline Steps - - /// Ensures VR per-eye intermediate textures exist at the correct resolution. - /// Must be called before any per-eye EncodeTexturesCS dispatch or PreparePerEyeInputs. - void EnsureVRIntermediateTextures(); - - /// Splits the combined stereo color buffer into per-eye intermediates, copies raw - /// motion vectors, and clears the HMD hidden area. FSR-only. - /// Reactive/transparency masks are written by EncodeTexturesCS. - void PreparePerEyeInputs(ID3D11Resource* colorSrc); - void FinalizePerEyeOutputs(ID3D11Resource* colorDst); - void ConfigureTAA(); void ConfigureUpscaling(RE::BSGraphics::State* a_state); void Upscale(); @@ -213,9 +177,7 @@ struct Upscaling : Feature /// Set by MenuOpenCloseEventHandler when LoadingMenu closes (cell/worldspace transitions, /// initial load). Consumed at the start of Upscale() to force a one-frame DLSS feature - /// rebuild — works around a VR-only persistent ~2-3ms GPU regression after worldspace - /// loads that otherwise only clears when the user manually toggles DLSS/preset. VR+DLSS - /// only; flat has no repro and per-eye extent asymmetry doesn't apply. + /// rebuild. std::atomic pendingDLSSReset{ false }; void CopySharedD3D12Resources(); diff --git a/src/Features/Upscaling/DX12SwapChain.cpp b/src/Features/Upscaling/DX12SwapChain.cpp index 430cf3a194..2dacad0ddf 100644 --- a/src/Features/Upscaling/DX12SwapChain.cpp +++ b/src/Features/Upscaling/DX12SwapChain.cpp @@ -37,18 +37,15 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC // Runtime format negotiation for swap chain DXGI_FORMAT attemptedFormat = DXGI_FORMAT_R10G10B10A2_UNORM; DXGI_FORMAT negotiatedFormat = DXGI_FORMAT_R10G10B10A2_UNORM; - bool isVR = REL::Module::IsVR(); bool fallbackUsed = false; - // Test R10G10B10A2 support (applies to both VR and non-VR for HDR capability) + // Test R10G10B10A2 support for HDR capability D3D12_FEATURE_DATA_FORMAT_SUPPORT formatSupport = { DXGI_FORMAT_R10G10B10A2_UNORM, D3D12_FORMAT_SUPPORT1_RENDER_TARGET, D3D12_FORMAT_SUPPORT2_NONE }; if (SUCCEEDED(d3d12Device->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &formatSupport, sizeof(formatSupport)))) { if ((formatSupport.Support1 & D3D12_FORMAT_SUPPORT1_RENDER_TARGET) == 0) { logger::warn("[DX12SwapChain] R10G10B10A2_UNORM not supported as render target, falling back to R8G8B8A8_UNORM"); negotiatedFormat = DXGI_FORMAT_R8G8B8A8_UNORM; fallbackUsed = true; - } else if (isVR) { - logger::info("[DX12SwapChain] VR detected with R10G10B10A2_UNORM support, attempting HDR"); } } else { logger::warn("[DX12SwapChain] CheckFeatureSupport failed for R10G10B10A2_UNORM, falling back to R8G8B8A8_UNORM"); @@ -56,10 +53,9 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC fallbackUsed = true; } - logger::info("[DX12SwapChain] Swap chain format negotiation: attempted={}, negotiated={}, VR={}, fallback={}", + logger::info("[DX12SwapChain] Swap chain format negotiation: attempted={}, negotiated={}, fallback={}", static_cast(attemptedFormat), static_cast(negotiatedFormat), - isVR ? "true" : "false", fallbackUsed ? "true" : "false"); swapChainDesc = {}; diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index b9976a99c7..1e19197234 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -152,9 +152,7 @@ void FidelityFX::Present(bool a_useFrameGeneration, bool a_isHDR) configParameters.flags = 0; configParameters.allowAsyncWorkloads = true; - auto state = globals::state; - - auto renderSize = state->screenSize * upscaling.resolutionScale; + auto renderSize = float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight } * upscaling.resolutionScale; configParameters.generationRect.left = (swapChain.swapChainDesc.Width - swapChain.swapChainDesc.Width) / 2; configParameters.generationRect.top = (swapChain.swapChainDesc.Height - swapChain.swapChainDesc.Height) / 2; @@ -243,8 +241,6 @@ void FidelityFX::Present(bool a_useFrameGeneration, bool a_isHDR) void FidelityFX::CreateFSRResources() { - auto state = globals::state; - // Prevent multiple allocations if (fsrScratchBuffer) { logger::warn("[FidelityFX] FSR resources already created, skipping allocation"); @@ -253,7 +249,7 @@ void FidelityFX::CreateFSRResources() auto fsrDevice = ffxGetDeviceDX11_Fsr31(globals::d3d::device); - uint32_t numContexts = globals::game::isVR ? 2 : 1; + uint32_t numContexts = 1; size_t scratchBufferSize = ffxGetScratchMemorySizeDX11(numContexts); fsrScratchBuffer = calloc(scratchBufferSize, 1); if (!fsrScratchBuffer) { @@ -270,51 +266,44 @@ void FidelityFX::CreateFSRResources() return; } - auto screenSize = state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; auto renderSize = Util::ConvertToDynamic(screenSize); - uint32_t displayWidth = (uint32_t)(globals::game::isVR ? screenSize.x / 2 : screenSize.x); + uint32_t displayWidth = (uint32_t)screenSize.x; uint32_t displayHeight = (uint32_t)screenSize.y; - uint32_t renderWidth = (uint32_t)(globals::game::isVR ? renderSize.x / 2 : renderSize.x); + uint32_t renderWidth = (uint32_t)renderSize.x; uint32_t renderHeight = (uint32_t)renderSize.y; - for (uint32_t i = 0; i < numContexts; ++i) { - FfxFsr3ContextDescription contextDescription; - contextDescription.maxRenderSize.width = renderWidth; - contextDescription.maxRenderSize.height = renderHeight; - contextDescription.maxUpscaleSize.width = displayWidth; - contextDescription.maxUpscaleSize.height = displayHeight; - contextDescription.displaySize.width = displayWidth; - contextDescription.displaySize.height = displayHeight; - contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE; - if (globals::features::hdrDisplay.loaded) { - contextDescription.flags |= FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; - contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R10G10B10A2_UNORM; - } else { - contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R8G8B8A8_UNORM; - } - contextDescription.backendInterfaceUpscaling = fsrInterface; - - if (ffxFsr3ContextCreate(&fsrContext[i], &contextDescription) != FFX_OK) { - logger::critical("[FidelityFX] Failed to initialize FSR3 context for eye {}!", i); - for (uint32_t j = 0; j < i; ++j) - ffxFsr3ContextDestroy(&fsrContext[j]); - free(fsrScratchBuffer); - fsrScratchBuffer = nullptr; - return; - } + FfxFsr3ContextDescription contextDescription; + contextDescription.maxRenderSize.width = renderWidth; + contextDescription.maxRenderSize.height = renderHeight; + contextDescription.maxUpscaleSize.width = displayWidth; + contextDescription.maxUpscaleSize.height = displayHeight; + contextDescription.displaySize.width = displayWidth; + contextDescription.displaySize.height = displayHeight; + contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE; + if (globals::features::hdrDisplay.loaded) { + contextDescription.flags |= FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; + contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R10G10B10A2_UNORM; + } else { + contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R8G8B8A8_UNORM; } - logger::info("[FidelityFX] Created {} FSR3 contexts (Display: {}x{}, Render: {}x{})", - numContexts, displayWidth, displayHeight, renderWidth, renderHeight); + contextDescription.backendInterfaceUpscaling = fsrInterface; + + if (ffxFsr3ContextCreate(&fsrContext[0], &contextDescription) != FFX_OK) { + logger::critical("[FidelityFX] Failed to initialize FSR3 context!"); + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + return; + } + logger::info("[FidelityFX] Created FSR3 context (Display: {}x{}, Render: {}x{})", + displayWidth, displayHeight, renderWidth, renderHeight); } void FidelityFX::DestroyFSRResources() { - uint32_t numContexts = globals::game::isVR ? 2 : 1; - for (uint32_t i = 0; i < numContexts; ++i) { - if (ffxFsr3ContextDestroy(&fsrContext[i]) != FFX_OK) - logger::critical("[FidelityFX] Failed to destroy FSR3 context for eye {}!", i); - } + if (ffxFsr3ContextDestroy(&fsrContext[0]) != FFX_OK) + logger::critical("[FidelityFX] Failed to destroy FSR3 context!"); // Free the scratch buffer to prevent memory leak if (fsrScratchBuffer) { @@ -351,97 +340,54 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto state = globals::state; auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - auto screenSize = state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; auto renderSize = Util::ConvertToDynamic(screenSize); auto& upscaling = globals::features::upscaling; auto jitter = upscaling.jitter; - auto DispatchFSR = [&](uint32_t contextIndex, ID3D11Resource* r_color, ID3D11Resource* r_depth, ID3D11Resource* r_mvec, - ID3D11Resource* r_reactive, ID3D11Resource* r_trans, ID3D11Resource* r_output, - uint32_t r_width, float mv_scale_x) { - if (state->frameAnnotations) { - if (globals::game::isVR) { - char buf[32]; - snprintf(buf, sizeof(buf), "FSR Dispatch Eye %u", contextIndex); - state->BeginPerfEvent(buf); - } else { - state->BeginPerfEvent("FSR Dispatch"); - } + if (state->frameAnnotations) + state->BeginPerfEvent("FSR Dispatch"); + + FfxFsr3DispatchUpscaleDescription dispatchParameters{}; + dispatchParameters.commandList = ffxGetCommandListDX11(context); + dispatchParameters.color = ffxGetResource(a_upscalingTexture, L"FSR3_Input_OutputColor"); + dispatchParameters.depth = ffxGetResource(depthTexture.texture, L"FSR3_InputDepth"); + dispatchParameters.motionVectors = ffxGetResource(a_motionVectors, L"FSR3_InputMotionVectors"); + dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); + dispatchParameters.upscaleOutput = ffxGetResource(a_upscalingTexture, L"FSR3_OutputColor"); + dispatchParameters.reactive = ffxGetResource(a_reactiveMask, L"FSR3_InputReactiveMap"); + dispatchParameters.transparencyAndComposition = ffxGetResource(a_transparencyCompositionMask, L"FSR3_TransparencyAndCompositionMap"); + + dispatchParameters.motionVectorScale.x = renderSize.x; + dispatchParameters.motionVectorScale.y = renderSize.y; + dispatchParameters.renderSize.width = (uint)renderSize.x; + dispatchParameters.renderSize.height = (uint)renderSize.y; + + dispatchParameters.jitterOffset.x = -jitter.x; + dispatchParameters.jitterOffset.y = -jitter.y; + + dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; + dispatchParameters.cameraFar = *globals::game::cameraFar; + dispatchParameters.cameraNear = *globals::game::cameraNear; + dispatchParameters.enableSharpening = true; + dispatchParameters.sharpness = a_sharpness; + dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); + dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; + dispatchParameters.reset = false; + dispatchParameters.preExposure = 1.0f; + dispatchParameters.flags = 0; + + __try { + if (ffxFsr3ContextDispatchUpscale(&fsrContext[0], &dispatchParameters) != FFX_OK) + logger::critical("[FidelityFX] Failed to dispatch upscaling!"); + } __except (EXCEPTION_EXECUTE_HANDLER) { + if (!fsrDispatchCrashLogged) { + logger::critical("[FidelityFX] FSR3 dispatch crashed - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture."); + fsrDispatchCrashLogged = true; } - - FfxFsr3DispatchUpscaleDescription dispatchParameters{}; - dispatchParameters.commandList = ffxGetCommandListDX11(context); - dispatchParameters.color = ffxGetResource(r_color, L"FSR3_Input_OutputColor"); - dispatchParameters.depth = ffxGetResource(r_depth, L"FSR3_InputDepth"); - dispatchParameters.motionVectors = ffxGetResource(r_mvec, L"FSR3_InputMotionVectors"); - dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); - dispatchParameters.upscaleOutput = ffxGetResource(r_output, L"FSR3_OutputColor"); - dispatchParameters.reactive = ffxGetResource(r_reactive, L"FSR3_InputReactiveMap"); - dispatchParameters.transparencyAndComposition = ffxGetResource(r_trans, L"FSR3_TransparencyAndCompositionMap"); - - dispatchParameters.motionVectorScale.x = mv_scale_x; - dispatchParameters.motionVectorScale.y = renderSize.y; - dispatchParameters.renderSize.width = r_width; - dispatchParameters.renderSize.height = (uint)renderSize.y; - - dispatchParameters.jitterOffset.x = -jitter.x; - dispatchParameters.jitterOffset.y = -jitter.y; - - dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; - dispatchParameters.cameraFar = *globals::game::cameraFar; - dispatchParameters.cameraNear = *globals::game::cameraNear; - dispatchParameters.enableSharpening = true; - dispatchParameters.sharpness = a_sharpness; - dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); - dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; - dispatchParameters.reset = false; - dispatchParameters.preExposure = 1.0f; - dispatchParameters.flags = 0; - - __try { - if (ffxFsr3ContextDispatchUpscale(&fsrContext[contextIndex], &dispatchParameters) != FFX_OK) - logger::critical("[FidelityFX] Failed to dispatch upscaling for eye {}!", contextIndex); - } __except (EXCEPTION_EXECUTE_HANDLER) { - if (!fsrDispatchCrashLogged) { - logger::critical("[FidelityFX] FSR3 dispatch crashed for eye {} - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture.", contextIndex); - fsrDispatchCrashLogged = true; - } - } - - if (state->frameAnnotations) - state->EndPerfEvent(); - }; - - if (globals::game::isVR) { - // Prepare per-eye inputs and clear mask - upscaling.PreparePerEyeInputs(a_upscalingTexture); - - uint32_t numViews = 2; - uint32_t eyeWidth = (uint32_t)(renderSize.x / 2); - for (uint32_t i = 0; i < numViews; ++i) { - DispatchFSR(i, - upscaling.vrIntermediateColorIn[i]->resource.get(), - upscaling.vrIntermediateLinearDepth[i]->resource.get(), - upscaling.vrIntermediateMotionVectors[i]->resource.get(), - upscaling.vrIntermediateReactiveMask[i]->resource.get(), - upscaling.vrIntermediateTransparencyMask[i]->resource.get(), - upscaling.vrIntermediateColorOut[i]->resource.get(), - eyeWidth, - renderSize.x / 2.0f); - } - - // Merge outputs back to kMAIN - upscaling.FinalizePerEyeOutputs(a_upscalingTexture); - } else { - DispatchFSR(0, - a_upscalingTexture, - depthTexture.texture, - a_motionVectors, - a_reactiveMask, - a_transparencyCompositionMask, - a_upscalingTexture, // Output to same texture - (uint)renderSize.x, - renderSize.x); } + + if (state->frameAnnotations) + state->EndPerfEvent(); } \ No newline at end of file diff --git a/src/Features/Upscaling/RCAS/RCAS.cpp b/src/Features/Upscaling/RCAS/RCAS.cpp index 46db9ef92e..576c14d714 100644 --- a/src/Features/Upscaling/RCAS/RCAS.cpp +++ b/src/Features/Upscaling/RCAS/RCAS.cpp @@ -48,8 +48,8 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces globals::profiler->BeginPass("Upscaling::RCAS"); state->BeginPerfEvent("RCAS Sharpening"); - uint32_t screenWidth = (uint32_t)state->screenSize.x; - uint32_t screenHeight = (uint32_t)state->screenSize.y; + uint32_t screenWidth = globals::game::graphicsState->screenWidth; + uint32_t screenHeight = globals::game::graphicsState->screenHeight; RCASConfig config{}; config.sharpness = sharpness; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index d7517c679f..503210393f 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -319,7 +319,7 @@ bool Streamline::EnsureFrameToken() return frameToken != nullptr; } -bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex) +bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport) { if (!globals::features::upscaling.streamline.initialized) return false; @@ -327,50 +327,27 @@ bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eye if (!EnsureFrameToken()) return false; - // In VR, we need to set constants for each viewport/eye separately - // In non-VR, this is called once per frame - auto state = globals::state; - sl::Constants slConstants = {}; - // Calculate aspect ratio for the SINGLE EYE - float eyeWidth = state->screenSize.x * (globals::game::isVR ? 0.5f : 1.0f); - slConstants.cameraAspectRatio = eyeWidth / state->screenSize.y; + slConstants.cameraAspectRatio = (float)globals::game::graphicsState->screenWidth / (float)globals::game::graphicsState->screenHeight; slConstants.cameraFOV = Util::GetVerticalFOVRad(); slConstants.cameraNear = *globals::game::cameraNear; slConstants.cameraFar = *globals::game::cameraFar; - auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse(eyeIndex).Transpose(); - auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered(eyeIndex).Transpose(); + auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse().Transpose(); + auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered().Transpose(); slConstants.cameraMotionIncluded = sl::Boolean::eTrue; slConstants.cameraPinholeOffset = { 0.f, 0.f }; slConstants.cameraRight = { viewMatrix._11, viewMatrix._12, viewMatrix._13 }; slConstants.cameraUp = { viewMatrix._21, viewMatrix._22, viewMatrix._23 }; slConstants.cameraFwd = { viewMatrix._31, viewMatrix._32, viewMatrix._33 }; - slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(eyeIndex); + slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(); slConstants.cameraViewToClip = *(sl::float4x4*)&cameraViewToClip; slConstants.depthInverted = sl::Boolean::eFalse; - if (globals::game::isVR) { - // VR: compute clipToCameraView / clipToPrevClip / prevClipToClip from Skyrim's per-eye matrices. - // recalculateCameraMatrices() uses a single static prev-frame slot -- unusable for two viewports. - sl::matrixFullInvert(slConstants.clipToCameraView, slConstants.cameraViewToClip); - - auto currViewProj = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Transpose(); - auto prevViewProj = globals::game::frameBufferCached.GetCameraPreviousViewProjUnjittered(eyeIndex).Transpose(); - - sl::float4x4 currViewProjSL = *(sl::float4x4*)&currViewProj; - sl::float4x4 prevViewProjSL = *(sl::float4x4*)&prevViewProj; - - sl::float4x4 invCurrViewProj; - sl::matrixFullInvert(invCurrViewProj, currViewProjSL); - sl::matrixMul(slConstants.clipToPrevClip, invCurrViewProj, prevViewProjSL); - sl::matrixFullInvert(slConstants.prevClipToClip, slConstants.clipToPrevClip); - } else { - recalculateCameraMatrices(slConstants); - } + recalculateCameraMatrices(slConstants); auto& upscaling = globals::features::upscaling; auto jitter = upscaling.jitter; @@ -385,7 +362,7 @@ bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eye slConstants.motionVectorsJittered = sl::Boolean::eFalse; if (SL_FAILED(res, slSetConstants(slConstants, *frameToken, p_viewport))) { - logger::error("[Streamline] Could not set constants for eye {}", eyeIndex); + logger::error("[Streamline] Could not set constants"); return false; } @@ -440,12 +417,10 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) break; } - auto state = globals::state; - dlssOptions.outputWidth = width; - dlssOptions.outputHeight = (uint)state->screenSize.y; + dlssOptions.outputHeight = (uint)globals::game::graphicsState->screenHeight; - // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 + // Detect HDR from kMAIN format at runtime { auto renderer = globals::game::renderer; auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; @@ -503,7 +478,7 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width) } } -void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, +void Streamline::EvaluateDLSS(sl::ViewportHandle vp, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth) @@ -517,7 +492,7 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, sl::Resource reactiveMaskRes = { sl::ResourceType::eTex2d, reactiveMask, 0 }; sl::Resource transparencyMaskRes = { sl::ResourceType::eTex2d, transparencyMask, 0 }; - if (!CheckFrameConstants(vp, eyeIndex)) + if (!CheckFrameConstants(vp)) return; const bool emitPCLMarkers = @@ -529,16 +504,14 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, return; const sl::Result markerResult = slPCLSetMarker(marker, *frameToken); if (markerResult != sl::Result::eOk) { - static bool markerErrorLogged[2][2] = { { false, false }, { false, false } }; - const uint32_t logIdx = globals::game::isVR ? std::min(eyeIndex, 1u) : 0u; + static bool markerErrorLogged[2] = { false, false }; const uint32_t boundedStageIndex = std::min(stageIndex, 1u); - if (markerErrorLogged[logIdx][boundedStageIndex]) + if (markerErrorLogged[boundedStageIndex]) return; - markerErrorLogged[logIdx][boundedStageIndex] = true; + markerErrorLogged[boundedStageIndex] = true; logger::warn( - "[Streamline] slPCLSetMarker({}) failed{}: {}", + "[Streamline] slPCLSetMarker({}) failed: {}", stageName, - globals::game::isVR ? std::format(" for eye {}", eyeIndex) : "", magic_enum::enum_name(markerResult)); } }; @@ -560,15 +533,8 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, const sl::BaseStructure* inputs[] = { &view }; auto state = globals::state; - if (state->frameAnnotations) { - if (globals::game::isVR) { - char buf[32]; - snprintf(buf, sizeof(buf), "DLSS Evaluate Eye %u", eyeIndex); - state->BeginPerfEvent(buf); - } else { - state->BeginPerfEvent("DLSS Evaluate"); - } - } + if (state->frameAnnotations) + state->BeginPerfEvent("DLSS Evaluate"); emitPCLMarker(sl::PCLMarker::eRenderSubmitStart, "DLSS-EvaluateStart", 0); sl::Result evalResult = slEvaluateFeature(sl::kFeatureDLSS, *frameToken, inputs, _countof(inputs), context); @@ -578,23 +544,20 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, state->EndPerfEvent(); if (evalResult != sl::Result::eOk) { - static bool evalErrorLogged[2] = { false, false }; - uint32_t logIdx = globals::game::isVR ? eyeIndex : 0; - if (!evalErrorLogged[logIdx]) { - evalErrorLogged[logIdx] = true; - logger::error("[Streamline] slEvaluateFeature failed{} result={}", globals::game::isVR ? std::format(" for eye {}", eyeIndex) : "", (int)evalResult); + static bool evalErrorLogged = false; + if (!evalErrorLogged) { + evalErrorLogged = true; + logger::error("[Streamline] slEvaluateFeature failed result={}", (int)evalResult); } } } void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors) { - auto state = globals::state; - auto renderer = globals::game::renderer; auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - auto screenSize = state->screenSize; + float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; auto renderSize = Util::ConvertToDynamic(screenSize); // When RCAS sharpening is active, direct DLSS output to sharpenerTexture so RCAS can @@ -603,86 +566,13 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r ID3D11Resource* colorOut = (upscaling.settings.sharpnessDLSS > 0.0f && upscaling.sharpenerTexture) ? upscaling.sharpenerTexture->resource.get() : a_upscalingTexture; - // VR stereo DLSS: NGX D3D11 only accepts zero-offset subrects. Non-zero offsets return - // FAIL_InvalidParameter because Streamline's dlssEntry.cpp never sets - // NVSDK_NGX_Parameter_DLSS_Enable_Output_Subrects during context creation. - // - // Both eyes copy their color slice into per-eye intermediates so ClearHMDMask can zero - // outside-mask regions before DLSS sees them (prevents temporal bleed into visible pixels). - // Eye 0 outputs directly to colorOut (zero-offset) — no intermediate output buffer needed. - // Eye 1 outputs to vrIntermediateColorOut[1] then copies back to kMAIN at eyeWidthOut. - // - // Eye 1 is pre-copied before eye 0 runs: at non-DLAA scales eye 0's upscaled output - // extends past eyeWidthIn into eye 1's input region of kMAIN. - if (globals::game::isVR) { - auto context = globals::d3d::context; - - uint32_t eyeWidthOut = (uint32_t)(screenSize.x / 2); - uint32_t eyeHeightOut = (uint32_t)screenSize.y; - uint32_t eyeWidthIn = (uint32_t)(renderSize.x / 2); - uint32_t eyeHeightIn = (uint32_t)renderSize.y; - - sl::Extent perEyeIn{ 0, 0, eyeWidthIn, eyeHeightIn }; - sl::Extent perEyeOut{ 0, 0, eyeWidthOut, eyeHeightOut }; - - // Both flags track the same creation pool (EnsureVRIntermediateTextures creates all - // intermediates atomically), so in practice eye0Ready == eye1Ready. The separate checks - // are kept for null-safety and to document which resources each eye path actually uses. - bool eye0Ready = upscaling.vrIntermediateColorIn[0] && - upscaling.vrIntermediateMotionVectors[0] && upscaling.vrIntermediateReactiveMask[0] && upscaling.vrIntermediateTransparencyMask[0]; - bool eye1Ready = upscaling.vrIntermediateColorIn[1] && upscaling.vrIntermediateColorOut[1] && - upscaling.vrIntermediateDepth && upscaling.vrIntermediateMotionVectors[1] && - upscaling.vrIntermediateReactiveMask[1] && upscaling.vrIntermediateTransparencyMask[1]; - - // Pre-copy eye 1 before eye 0 runs (overlap hazard), then clear HMD mask. - if (eye1Ready) { - D3D11_BOX rightIn = { eyeWidthIn, 0, 0, eyeWidthIn * 2, eyeHeightIn, 1 }; - context->CopySubresourceRegion(upscaling.vrIntermediateColorIn[1]->resource.get(), 0, 0, 0, 0, a_upscalingTexture, 0, &rightIn); - context->CopySubresourceRegion(upscaling.vrIntermediateDepth->resource.get(), 0, 0, 0, 0, depthTexture.texture, 0, &rightIn); - upscaling.ClearHMDMask(upscaling.vrIntermediateColorIn[1]->uav.get(), depthTexture.depthSRV, - eyeWidthIn, eyeHeightIn, eyeWidthIn, 0); - } - - // Eye 0: copy left-eye slice, clear HMD mask, output directly to colorOut at offset 0. - if (eye0Ready) { - D3D11_BOX leftIn = { 0, 0, 0, eyeWidthIn, eyeHeightIn, 1 }; - context->CopySubresourceRegion(upscaling.vrIntermediateColorIn[0]->resource.get(), 0, 0, 0, 0, a_upscalingTexture, 0, &leftIn); - upscaling.ClearHMDMask(upscaling.vrIntermediateColorIn[0]->uav.get(), depthTexture.depthSRV, - eyeWidthIn, eyeHeightIn, 0, 0); - - EvaluateDLSS(viewport, 0, - upscaling.vrIntermediateColorIn[0]->resource.get(), colorOut, - depthTexture.texture, - upscaling.vrIntermediateMotionVectors[0]->resource.get(), - upscaling.vrIntermediateReactiveMask[0]->resource.get(), - upscaling.vrIntermediateTransparencyMask[0]->resource.get(), - perEyeIn, perEyeOut, eyeWidthOut); - } + sl::Extent extentIn{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; + sl::Extent extentOut{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; - // Eye 1: evaluate into intermediate, then copy upscaled result to kMAIN right-eye position. - if (eye1Ready) { - EvaluateDLSS(viewportRight, 1, - upscaling.vrIntermediateColorIn[1]->resource.get(), - upscaling.vrIntermediateColorOut[1]->resource.get(), - upscaling.vrIntermediateDepth->resource.get(), - upscaling.vrIntermediateMotionVectors[1]->resource.get(), - upscaling.vrIntermediateReactiveMask[1]->resource.get(), - upscaling.vrIntermediateTransparencyMask[1]->resource.get(), - perEyeIn, perEyeOut, eyeWidthOut); - - D3D11_BOX rightOut = { 0, 0, 0, eyeWidthOut, eyeHeightOut, 1 }; - context->CopySubresourceRegion(colorOut, 0, eyeWidthOut, 0, 0, upscaling.vrIntermediateColorOut[1]->resource.get(), 0, &rightOut); - } - } else { - // Non-VR: Simple full-texture upscale. - sl::Extent extentIn{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; - sl::Extent extentOut{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; - - EvaluateDLSS(viewport, 0, - a_upscalingTexture, colorOut, - depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask, - extentIn, extentOut, (uint)screenSize.x); - } + EvaluateDLSS(viewport, + a_upscalingTexture, colorOut, + depthTexture.texture, a_motionVectors, a_reactiveMask, a_transparencyCompositionMask, + extentIn, extentOut, (uint)screenSize.x); } void Streamline::UpdateReflex() @@ -774,9 +664,4 @@ void Streamline::DestroyDLSSResources() slDLSSSetOptions(viewport, dlssOptions); slFreeResources(sl::kFeatureDLSS, viewport); - - if (globals::game::isVR) { - slDLSSSetOptions(viewportRight, dlssOptions); - slFreeResources(sl::kFeatureDLSS, viewportRight); - } } diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index f173dd1bde..2d2abbf6cf 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -38,7 +38,6 @@ class Streamline bool reflexSupportedOnCurrentAdapter = false; sl::ViewportHandle viewport{ 0 }; - sl::ViewportHandle viewportRight{ 1 }; static constexpr uint32_t MAX_RESOLUTION = 8192; HMODULE interposer = NULL; @@ -88,7 +87,7 @@ class Streamline uint32_t lastReflexSleepFrame = UINT32_MAX; // Helper: Execute DLSS for a single viewport with given resources - void EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, + void EvaluateDLSS(sl::ViewportHandle vp, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth); @@ -103,7 +102,7 @@ class Streamline void PostDevice(); bool EnsureFrameToken(); - bool CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex = 0); + bool CheckFrameConstants(sl::ViewportHandle p_viewport); bool IsRTXAndBelow40Series(IDXGIAdapter* a_adapter); diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp deleted file mode 100644 index 6f0b676a5d..0000000000 --- a/src/Features/VR.cpp +++ /dev/null @@ -1,294 +0,0 @@ -#include "VR.h" -#include "Menu.h" -#include "RE/B/BSOpenVR.h" -#include "RE/P/PlayerCharacter.h" -#include "Upscaling.h" -#include "VR/OpenVRDetection.h" - -#include "State.h" -#include "Utils/D3D.h" -#include "Utils/VRUtils.h" - -#include -#include -#include - -using AttachMode = VR::Settings::OverlayAttachMode; - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( - VR::Settings, - EnableDepthBufferCullingInterior, - EnableDepthBufferCullingExterior, - MinOccludeeBoxExtent, - VRMenuScale, - VRMenuPositioningMethod, - attachMode, - VRMenuAttachController, - VRMenuOffsetX, - VRMenuOffsetY, - VRMenuOffsetZ, - VRMenuControllerOffsetX, - VRMenuControllerOffsetY, - VRMenuControllerOffsetZ, - mouseDeadzone, - mouseSpeed, - dragHighlightColor, - VRMenuOpenKeys, - VRMenuCloseKeys, - VROverlayOpenKeys, - VROverlayCloseKeys, - comboTimeout, - EnableDragToReposition, - kAutoHideSeconds, - VRMenuAutoResetDistance, - EnableWandPointing, - EnableStereoBlend, - StereoBlendDepthSigma, - StereoBlendMaxFactor, - StereoBlendColorThreshold, - StereoBlendDebugMode) - -//============================================================================= -// FEATURE BASE CLASS OVERRIDES -//============================================================================= - -void VR::LoadSettings(json& o_json) -{ - settings = o_json.get(); - settings.ClampToValidRanges(); - if (o_json.contains("StereoOptimizations")) { - json stereoOptJson = o_json["StereoOptimizations"]; - stereoOpt.LoadSettings(stereoOptJson); - } -} - -void VR::SaveSettings(json& o_json) -{ - o_json = settings; - { - json stereoOptJson; - stereoOpt.SaveSettings(stereoOptJson); - o_json["StereoOptimizations"] = stereoOptJson; - } -} - -void VR::RestoreDefaultSettings() -{ - settings = {}; - stereoOpt.RestoreDefaultSettings(); -} - -void VR::SetupResources() -{ - CompileStereoBlendShaders(); - - auto renderer = globals::game::renderer; - auto mainTex = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - D3D11_TEXTURE2D_DESC mainDesc; - mainTex.texture->GetDesc(&mainDesc); - mainDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; - mainDesc.MiscFlags = 0; - stereoBlendCopyTex = eastl::make_unique(mainDesc, "VR::StereoBlendCopyTex"); - D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { - .Format = mainDesc.Format, - .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, - .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } - }; - stereoBlendCopyTex->CreateSRV(srvDesc); - stereoBlendCB = eastl::make_unique(ConstantBufferDesc(), "VR::StereoBlendCB"); - - if (globals::game::isVR && stereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { - stereoOpt.SetupResources(); - stereoOpt.loaded = - stereoOpt.GetModeTextureSRV() != nullptr && - stereoOpt.GetPomOffsetSRV() != nullptr && - stereoOpt.GetPomOffsetUAV() != nullptr; - } else { - stereoOpt.loaded = false; - } - - DetectOpenVRInfo(); - - if (openVRInfo.isAvailable) { - logger::info("OpenVR DLL detected:"); - logger::info(" Path: {}", openVRInfo.dllPath); - logger::info(" Version: {}", openVRInfo.version); - logger::info(" Size: {} bytes", openVRInfo.fileSize); - logger::info(" Modified: {}", openVRInfo.modificationTime); - logger::info(" Runtime: {}", VRDetection::RuntimeTypeToString(openVRInfo.runtimeType)); - logger::info(" Interface probing: {}", openVRInfo.probingSucceeded ? "Passed" : "Failed"); - logger::info(" Overlay (IVROverlay_016): {}", openVRInfo.hasOverlayInterface ? "Yes" : "No"); - logger::info(" System (IVRSystem_017): {}", openVRInfo.hasSystemInterface ? "Yes" : "No"); - logger::info(" Compositor (IVRCompositor_021): {}", openVRInfo.hasCompositorInterface ? "Yes" : "No"); - logger::info(" Compatible: {}", openVRInfo.isCompatible ? "Yes" : "No"); - - if (!openVRInfo.isCompatible) { - if (globals::state->IsDeveloperMode()) { - logger::info("OpenVR not natively compatible, but developer mode is active - VR menus enabled"); - } else { - logger::info("OpenVR version is incompatible. Community Shaders VR menus will be disabled for stability"); - } - } - } else { - logger::info("OpenVR DLL not available in current process"); - } -} - -void VR::PostPostLoad() -{ - gDepthBufferCulling = reinterpret_cast(REL::Offset(0x1EC6B88).address()); - if (!gDepthBufferCulling) { - static bool s_defaultDepthBufferCulling = false; - gDepthBufferCulling = &s_defaultDepthBufferCulling; - logger::warn("VR: gDepthBufferCulling address not found - using fallback default (false)"); - } - - gMinOccludeeBoxExtent = reinterpret_cast(REL::Offset(0x1ED64E8).address()); - if (!gMinOccludeeBoxExtent) { - static float s_defaultMinOccludeeBoxExtent = 10.0f; - gMinOccludeeBoxExtent = &s_defaultMinOccludeeBoxExtent; - logger::warn("VR: gMinOccludeeBoxExtent address not found - using fallback default (10.0)"); - } - - // Migration: Fix legacy overlay keybinds - if (settings.VROverlayCloseKeys.size() == 1) { - auto& closeKey = settings.VROverlayCloseKeys[0]; - if (closeKey.GetDevice() == ControllerDevice::Keyboard && closeKey.GetKey() == 32) { - settings.VROverlayCloseKeys[0] = InputCombo::Primary(32); - logger::info("VR: Migrated VROverlayCloseKeys from Keyboard(32) to Primary(32)"); - } - } - if (settings.VROverlayOpenKeys.size() == 1) { - auto& openKey = settings.VROverlayOpenKeys[0]; - if (openKey.GetDevice() == ControllerDevice::Keyboard && openKey.GetKey() == 32) { - settings.VROverlayOpenKeys[0] = InputCombo::Secondary(32); - logger::info("VR: Migrated VROverlayOpenKeys from Keyboard(32) to Secondary(32)"); - } - } - - REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xD9) + 0x2, 0x148); - REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xE5) + 0x2, 0x14C); - REL::safe_write(REL::RelocationID(0, 0, 69528).address() + REL::Relocate(0, 0, 0xF1) + 0x2, 0x150); -} - -void VR::DataLoaded() -{ - // Initialize occlusion culling based on user settings and current interior/exterior state. - UpdateDepthBufferCulling(); - - if (gMinOccludeeBoxExtent) { - *gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; - } else { - logger::warn("VR::DataLoaded: gMinOccludeeBoxExtent is null, skipping assignment"); - } -} - -void VR::EarlyPrepass() -{ - // Apply culling setting each prepass based on current interior/exterior state. - UpdateDepthBufferCulling(); -} - -//============================================================================= -// OVERLAY SUBMIT AND DEPTH BUFFER CULLING -//============================================================================= - -void VR::RecreateOverlayTexturesIfNeeded() -{ - Util::CreateOverlayTextureAndRTV(globals::d3d::device, Config::kOverlayWidth, Config::kOverlayHeight, menuTexture.put(), menuRTV.put()); -} - -bool VR::IsWelcomeOverlayVisible() const -{ - return settings.kAutoHideSeconds > 0 && - globals::game::ui && - globals::game::ui->IsMenuOpen(RE::MainMenu::MENU_NAME) && - globals::menu && - !globals::menu->IsEnabled; -} - -void VR::SubmitOverlayFrame() -{ - InstallSubmitHook(); - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) { - return; - } - - auto& enabled = globals::menu->IsEnabled; - auto& overlayVisible = globals::menu->overlayVisible; - - if ((enabled || overlayVisible || IsWelcomeOverlayVisible()) && menuTexture.get() && menuRTV.get()) { - UpdateFixedWorldPositioning(); - UpdateOverlayDrag(); - - ID3D11RenderTargetView* oldRTV = nullptr; - globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); - ID3D11RenderTargetView* menuRTVPtr = menuRTV.get(); - globals::d3d::context->OMSetRenderTargets(1, &menuRTVPtr, nullptr); - float clearColor[4] = { 0, 0, 0, 0 }; - globals::d3d::context->ClearRenderTargetView(menuRTV.get(), clearColor); - ImGui::Render(); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); - - bool beingDragged = settings.EnableDragToReposition && overlayDragState.dragging; - Util::ApplyHighlightTintToTexture(menuTexture.get(), beingDragged, settings.dragHighlightColor); - - if (oldRTV) - oldRTV->Release(); - } -} - -// Helper to centralize VR depth buffer culling logic, reducing duplication between DataLoaded, EarlyPrepass, and Settings UI. -void VR::UpdateDepthBufferCulling() -{ - if (!gDepthBufferCulling) { - return; - } - - const auto* tes = globals::game::tes; - const bool inInterior = tes && tes->interiorCell != nullptr; - const bool desired = inInterior ? settings.EnableDepthBufferCullingInterior : settings.EnableDepthBufferCullingExterior; - - const bool previous = *gDepthBufferCulling; - *gDepthBufferCulling = desired; - - if (previous != desired) { - logger::info("VR depth buffer culling set to {}", desired); - } -} - -//============================================================================= -// OPENVR VERSION DETECTION AND COMPATIBILITY -//============================================================================= - -void VR::DetectOpenVRInfo() -{ - openVRInfo = {}; - - auto result = VRDetection::Detect(); - - openVRInfo.isAvailable = result.isAvailable; - openVRInfo.isCompatible = result.isCompatible; - openVRInfo.dllPath = result.dllPath; - openVRInfo.version = result.version; - openVRInfo.fileSize = result.fileSize; - openVRInfo.modificationTime = result.modificationTime; - openVRInfo.hasOverlayInterface = result.hasOverlayInterface; - openVRInfo.hasSystemInterface = result.hasSystemInterface; - openVRInfo.hasCompositorInterface = result.hasCompositorInterface; - openVRInfo.runtimeType = result.runtimeType; - openVRInfo.probingSucceeded = result.probingSucceeded; -} - -bool VR::IsOpenVRCompatible() const -{ - return globals::game::isVR && openVRInfo.isCompatible; -} - -void VR::Reset() -{ - stereoOpt.Reset(); -} diff --git a/src/Features/VR.h b/src/Features/VR.h deleted file mode 100644 index 1c63d30fff..0000000000 --- a/src/Features/VR.h +++ /dev/null @@ -1,558 +0,0 @@ -#pragma once -#include "Menu.h" -#include "OverlayFeature.h" -#include "Utils/Input.h" -#include "VR/OpenVRDetection.h" // In Features/VR/ -#include "VRStereoOptimizations.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace DirectX::SimpleMath; - -// Backwards compatibility aliases -using ControllerDevice = InputDeviceType; -using ButtonCombo = InputCombo; - -/** - * @brief Main VR feature class providing VR-specific optimizations and overlay UI system - * - * This class extends OverlayFeature to provide comprehensive VR support including: - * - Performance optimizations (depth buffer culling, occlusion culling) - * - VR overlay system for in-game UI interaction - * - Controller input processing and button combo mapping - * - Overlay positioning and manipulation (HMD-relative, controller-relative, fixed world) - * - Drag-and-drop overlay repositioning - * - * The VR class follows the singleton pattern and integrates with the OpenVR API - * to provide seamless VR experience within the Community Shaders framework. - * - * @example - * ```cpp - * // Get the VR singleton instance - * VR* vr = VR::GetSingleton(); - * - * // Check if VR is supported - * if (vr->SupportsVR()) { - * // Configure VR settings - * vr->settings.EnableDepthBufferCulling = true; - * vr->settings.VRMenuScale = 1.2f; - * } - * ``` - */ -struct VR : OverlayFeature -{ -public: - //============================================================================= - // NESTED TYPES AND CONSTANTS - //============================================================================= - - /** - * @brief Configuration constants for VR feature defaults and limits - * - * These constants define the default values and valid ranges for various - * VR settings to ensure consistent behavior and prevent invalid configurations. - */ - struct Config - { - // Overlay texture dimensions - static constexpr int kOverlayWidth = 1920; ///< Overlay texture width in pixels - static constexpr int kOverlayHeight = 1080; ///< Overlay texture height in pixels - static constexpr float kOverlayAspect = static_cast(kOverlayHeight) / static_cast(kOverlayWidth); ///< Aspect ratio (height/width) - - static inline Matrix CreateOverlayScaleMatrix(float scale) - { - return Matrix::CreateScale(scale, scale * kOverlayAspect, scale); - } - - static constexpr float kDefaultMenuScale = 1.0f; ///< Default overlay scale factor - static constexpr float kMinMenuScale = 0.1f; ///< Minimum allowed overlay scale - static constexpr float kMaxMenuScale = 5.0f; ///< Maximum allowed overlay scale - static constexpr float kDefaultComboTimeout = 3.0f; ///< Default timeout for button combos (seconds) - static constexpr float kDefaultMouseDeadzone = 0.1f; ///< Default thumbstick deadzone for mouse input - static constexpr float kDefaultMouseSpeed = 10.0f; ///< Default mouse speed multiplier - static constexpr int kDefaultAutoHideSeconds = 30; ///< Default auto-hide timeout for overlay messages - static constexpr int kMaxAutoHideSeconds = 300; ///< Maximum auto-hide timeout (5 minutes) - - // Default HMD overlay offset values (in meters, relative to HMD) - static constexpr float kDefaultHMDOffsetX = 0.195f; ///< Default horizontal offset from HMD - static constexpr float kDefaultHMDOffsetY = -0.375f; ///< Default vertical offset from HMD - static constexpr float kDefaultHMDOffsetZ = -1.355f; ///< Default depth offset from HMD - - // Default controller overlay offset values (in meters, relative to controller) - static constexpr float kDefaultControllerOffsetX = 0.295f; ///< Default horizontal offset from controller - static constexpr float kDefaultControllerOffsetY = 0.211f; ///< Default vertical offset from controller - static constexpr float kDefaultControllerOffsetZ = 0.063f; ///< Default depth offset from controller - }; - - //============================================================================= - // FEATURE BASE CLASS OVERRIDES - //============================================================================= - - virtual inline std::string GetName() override { return "VR"; } - virtual std::string GetDisplayName() override { return T("feature.vr.name", "VR"); } - virtual inline std::string GetShortName() override { return "VR"; } - virtual std::pair> GetFeatureSummary() override - { - return { T("feature.vr.description", "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments."), - { T("feature.vr.key_feature_1", "Depth buffer culling optimization for VR performance"), - T("feature.vr.key_feature_2", "In-scene overlay menu with HMD/Controller/Fixed World attach modes"), - T("feature.vr.key_feature_3", "VR controller input with customizable button mappings"), - T("feature.vr.key_feature_4", "Grip-to-drag overlay positioning with depth control"), - T("feature.vr.key_feature_5", "Configurable occlusion culling parameters"), - T("feature.vr.key_feature_6", "Enhanced VR compatibility with SteamVR and OpenComposite") } }; - }; - - virtual inline std::string_view GetShaderDefineName() override { return "VR_STEREO_OPT"; } - virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return stereoOpt.CanDispatchStencil() && (t == RE::BSShader::Type::Utility || t == RE::BSShader::Type::Lighting); } - virtual void Reset() override; - virtual void SetupResources() override; - virtual void ClearShaderCache() override; - virtual bool SupportsVR() override { return true; } - virtual bool IsCore() const override { return true; } - - virtual void PostPostLoad() override; - virtual void DataLoaded() override; - virtual void EarlyPrepass() override; - - void UpdateDepthBufferCulling(); - - // Stereo bilateral blend pass - called from Deferred::DeferredPasses after composite - void DrawStereoBlend(); - void CompileStereoBlendShaders(); - bool IsStereoOptimizationCullingReady() const - { - return REL::Module::IsVR() && - stereoOpt.CanDispatchStencil() && - stereoBlendOverwriteCS && - stereoBlendCopyTex && - stereoBlendCB; - } - static bool AnyScreenSpaceEffectLoaded(); - - virtual void LoadSettings(json& o_json) override; - virtual void SaveSettings(json& o_json) override; - virtual void RestoreDefaultSettings() override; - - virtual void DrawSettings() override; - - virtual std::string_view GetCategory() const override { return FeatureCategories::kUtility; } - - //============================================================================= - // OVERLAY FEATURE OVERRIDES - //============================================================================= - - virtual void DrawOverlay() override; - virtual bool IsOverlayVisible() const override { return IsOpenVRCompatible() && settings.kAutoHideSeconds > 0 && globals::menu && !globals::menu->IsEnabled; } - - //============================================================================= - // SETTINGS STRUCTURE - //============================================================================= - - /** - * @brief Configuration settings for the VR feature - * - * This structure contains all user-configurable settings for VR functionality, - * including performance optimizations, overlay positioning, input mapping, and - * visual customization options. Settings are automatically validated and clamped - * to valid ranges when loaded or modified. - */ - struct Settings - { - // Performance optimization settings - bool EnableDepthBufferCullingExterior = true; ///< Enable depth buffer culling for VR performance - bool EnableDepthBufferCullingInterior = true; - float MinOccludeeBoxExtent = 10.0f; ///< Minimum bounding box size for occlusion culling - - // Stereo consistency blend pass (post-composite safety net) - bool EnableStereoBlend = false; ///< Enable depth-aware bilateral blend between eyes - float StereoBlendDepthSigma = 0.01f; ///< Depth sensitivity for bilateral weight (lower = stricter) - float StereoBlendMaxFactor = 0.1f; ///< Maximum blend factor; keep low to preserve stereo parallax - float StereoBlendColorThreshold = 0.02f; ///< Minimum color difference to trigger blending (luminance) - int StereoBlendDebugMode = 0; ///< 0=off, 1=back-check, 2=blend weight, 3=edge detection, 4=overwrite, 5=overwrite Eye1 - - // VR Menu Overlay positioning settings - float VRMenuScale = Config::kDefaultMenuScale; ///< Scale factor for overlay UI (0.5-2.0) - int VRMenuPositioningMethod = 1; ///< 0 = HMD relative, 1 = Fixed world position - - /** - * @brief Defines how overlays are attached and positioned in VR space - */ - enum class OverlayAttachMode - { - HMDOnly = 0, ///< Overlay attached to HMD only - ControllerOnly = 1, ///< Overlay attached to controller only - Both = 2, ///< Overlay can be attached to both HMD and controller - None = 3 ///< Overlay display disabled - }; - OverlayAttachMode attachMode = OverlayAttachMode::HMDOnly; ///< Current overlay attachment mode - ControllerDevice VRMenuAttachController = ControllerDevice::Secondary; ///< Which controller to attach overlay to - - // HMD overlay offset settings (in meters) - float VRMenuOffsetX = Config::kDefaultHMDOffsetX; ///< Horizontal offset from HMD - float VRMenuOffsetY = Config::kDefaultHMDOffsetY; ///< Vertical offset from HMD - float VRMenuOffsetZ = Config::kDefaultHMDOffsetZ; ///< Depth offset from HMD - - // Controller overlay offset settings (in meters) - float VRMenuControllerOffsetX = Config::kDefaultControllerOffsetX; ///< Horizontal offset from controller - float VRMenuControllerOffsetY = Config::kDefaultControllerOffsetY; ///< Vertical offset from controller - float VRMenuControllerOffsetZ = Config::kDefaultControllerOffsetZ; ///< Depth offset from controller - - // Input and interaction settings - bool VRMenuControllerDiagnosticsTestMode = false; ///< Enable controller diagnostics mode - float mouseDeadzone = Config::kDefaultMouseDeadzone; ///< Thumbstick deadzone for mouse input (0.0-1.0) - float mouseSpeed = Config::kDefaultMouseSpeed; ///< Mouse speed multiplier (0.1-50.0) - - // Wand pointing settings - bool EnableWandPointing = true; ///< Enable controller wand/ray-cast pointing (modern VR input) - - // Visual customization - std::array dragHighlightColor = { 1.0f, 1.0f, 0.0f, 0.3f }; ///< RGBA color for drag highlight - - // Key binding configurations - std::vector VRMenuOpenKeys = { ///< Button combos to open VR menu - ButtonCombo::Secondary(static_cast(RE::BSOpenVRControllerDevice::Keys::kXA)), - ButtonCombo::Secondary(static_cast(RE::BSOpenVRControllerDevice::Keys::kBY)) - }; - std::vector VRMenuCloseKeys = { ///< Button combos to close VR menu - ButtonCombo::Both(static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip)) - }; - std::vector VROverlayOpenKeys = { ///< Button combos to open VR overlay - ButtonCombo::Secondary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) - }; - std::vector VROverlayCloseKeys = { ///< Button combos to close VR overlay - ButtonCombo::Primary(static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger)) - }; - - // General interaction settings - float comboTimeout = Config::kDefaultComboTimeout; ///< Timeout for button combo sequences (1.0-10.0 seconds) - int kAutoHideSeconds = Config::kDefaultAutoHideSeconds; ///< Auto-hide timeout for overlay messages (>0 shows overlay, <=0 hides it) - bool EnableDragToReposition = false; ///< Allow drag-and-drop overlay repositioning - - float VRMenuAutoResetDistance = 1000.0f; // Default: 1000 units ≈ 14.3 meters - - /** - * @brief Validates if the current menu scale is within acceptable range - * @return true if scale is between kMinMenuScale and kMaxMenuScale - */ - bool IsMenuScaleValid() const - { - return VRMenuScale >= Config::kMinMenuScale && VRMenuScale <= Config::kMaxMenuScale; - } - - /** - * @brief Validates if the current attach mode is valid - * @return true if attach mode is within valid enum range - */ - bool IsAttachModeValid() const - { - return attachMode >= OverlayAttachMode::HMDOnly && attachMode <= OverlayAttachMode::None; - } - - /** - * @brief Clamps all settings to their valid ranges - * - * This method ensures all numeric settings are within acceptable bounds, - * automatically correcting any out-of-range values that might have been - * loaded from configuration files or set programmatically. - */ - void ClampToValidRanges() - { - VRMenuScale = std::clamp(VRMenuScale, Config::kMinMenuScale, Config::kMaxMenuScale); - mouseDeadzone = std::clamp(mouseDeadzone, 0.0f, 1.0f); - mouseSpeed = std::clamp(mouseSpeed, 0.1f, 50.0f); - comboTimeout = std::clamp(comboTimeout, 1.0f, 10.0f); - kAutoHideSeconds = std::clamp(kAutoHideSeconds, 0, Config::kMaxAutoHideSeconds); - StereoBlendDepthSigma = std::clamp(StereoBlendDepthSigma, 0.001f, 0.1f); - StereoBlendMaxFactor = std::clamp(StereoBlendMaxFactor, 0.0f, 0.5f); - StereoBlendColorThreshold = std::clamp(StereoBlendColorThreshold, 0.0f, 0.2f); - StereoBlendDebugMode = std::clamp(StereoBlendDebugMode, 0, 5); - } - }; - - Settings settings; ///< Current VR configuration settings - - //============================================================================= - // VR-SPECIFIC PUBLIC API - //============================================================================= - - void ProcessVREvents(std::vector& vrEvents); - - // Wand pointing methods - enum class OverlayType - { - HMD, - Controller - }; - bool ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); - bool ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV); - void UpdateCursorFromWandPointing(); - void UpdateOverlayMenuStateFromInput(); - void ProcessVRButtonEvent(const Menu::KeyEvent& event); - void UpdateControllerState(const Menu::KeyEvent& event); - void ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io); - void ProcessControllerInputForImGui(); - - void RecreateOverlayTexturesIfNeeded(); - void SubmitOverlayFrame(); - bool IsWelcomeOverlayVisible() const; - - /** - * @brief Context for rendering VR overlays with render target management - */ - struct OverlayRenderContext - { - vr::IVROverlay* gameOverlay; - vr::IVROverlay* cleanOverlay; - RE::BSOpenVR* openvr; - ID3D11RenderTargetView* oldRTV = nullptr; - float clearColor[4] = { 0, 0, 0, 0 }; - - bool IsValid() const - { - return gameOverlay && cleanOverlay && openvr && openvr->vrSystem; - } - - void SaveRenderTarget() - { - globals::d3d::context->OMGetRenderTargets(1, &oldRTV, nullptr); - } - - void RestoreRenderTarget() - { - globals::d3d::context->OMSetRenderTargets(1, &oldRTV, nullptr); - if (oldRTV) { - oldRTV->Release(); - oldRTV = nullptr; - } - } - - void RenderToTexture(ID3D11RenderTargetView* targetRTV) - { - globals::d3d::context->OMSetRenderTargets(1, &targetRTV, nullptr); - globals::d3d::context->ClearRenderTargetView(targetRTV, clearColor); - ImGui::Render(); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - } - }; - - void SubmitHMDOverlay(OverlayRenderContext& context); - void SubmitControllerOverlay(OverlayRenderContext& context); - void HideAllOverlays(vr::IVROverlay* gameOverlay); - - void UpdateOverlayDrag(); - bool CanPerformDrag(); - void UpdateActiveDrag(); - void TryStartNewDrag(); - void SetFixedOverlayToCurrentHMD(); - void UpdateFixedWorldPositioning(); - bool ShouldHighlightOverlayWindow() const { return overlayDragState.dragging; } - - //============================================================================= - // PUBLIC MEMBER VARIABLES - //============================================================================= - - // OpenVR overlay handles and DirectX 11 rendering resources - vr::VROverlayHandle_t menuOverlayHandle = vr::k_ulOverlayHandleInvalid; - vr::VROverlayHandle_t menuControllerOverlayHandle = vr::k_ulOverlayHandleInvalid; - winrt::com_ptr menuTexture; - winrt::com_ptr menuRTV; - winrt::com_ptr menuControllerTexture; - winrt::com_ptr menuControllerRTV; - - // Stereo blend compute shader resources - winrt::com_ptr stereoBlendCS; - winrt::com_ptr stereoBlendDebugBackCheckCS; - winrt::com_ptr stereoBlendDebugBlendWeightCS; - winrt::com_ptr stereoBlendDebugEdgeDetectionCS; - winrt::com_ptr stereoBlendOverwriteCS; - eastl::unique_ptr stereoBlendCopyTex; - eastl::unique_ptr stereoBlendCB; - winrt::com_ptr stereoBlendLinearSampler; - - VRStereoOptimizations stereoOpt; - - struct alignas(16) StereoBlendCB - { - float FrameDim[2]; - float RcpFrameDim[2]; - float DepthSigma; - float MaxBlendFactor; - float ColorDiffThreshold; - float DebugEdgeTint; - uint32_t DebugMode; - float FullBlendDistance; - float POMDepthScale; - float _pad; - }; - - // Engine hook integration points - bool* gDepthBufferCulling = nullptr; - float* gMinOccludeeBoxExtent = nullptr; - - // VR Controller state and logging - struct VRControllerEventLog - { - int device; - int keyCode; - int value; - bool pressed; - double heldTime; - std::string heldSource; - float thumbstickX = 0.0f; - float thumbstickY = 0.0f; - std::string controllerRole; - }; - - std::vector vrControllerEventLog; - RE::VRControllerState primaryControllerState; - RE::VRControllerState secondaryControllerState; - bool lastKnownLeftHandedMode = false; - - struct OverlayWorldPosition - { - Matrix m = Matrix::Identity; - bool initialized = false; - } fixedWorldOverlayPosition; - - struct OverlayDragState - { - bool dragging = false; - vr::TrackedDeviceIndex_t controllerIndex = vr::k_unTrackedDeviceIndexInvalid; - bool isPrimary = false; - bool isSecondary = false; - Matrix initialControllerMatrix = Matrix::Identity; - Matrix initialOverlayMatrix = Matrix::Identity; - Matrix grabOffset = Matrix::Identity; - bool intersecting = false; - - enum class DragMode - { - None, - FixedWorld, - HMD, - Controller - } mode = DragMode::None; - - Vector3 initialHMDOffset = Vector3::Zero; - Vector3 initialControllerOffset = Vector3::Zero; - float initialHMDScale = 1.0f; - Matrix startControllerMatrix = Matrix::Identity; - } overlayDragState; - - struct ComboSequence - { - std::vector sequence; - double startTime = 0.0; - size_t currentIndex = 0; - bool active = false; - }; - ComboSequence menuOpenCombo; - ComboSequence menuCloseCombo; - - enum class ComboType - { - None, - MenuOpen, - MenuClose, - OverlayOpen, - OverlayClose - }; - - bool isCapturingCombo = false; - ComboType currentComboType = ComboType::None; - const char* currentComboName = nullptr; - std::vector recordedCombo; - double comboStartTime = 0.0; - double comboTimeout = 3.0; - - // Button controller recording state for UI settings - std::unordered_map recordingButtonControllers; - - // OpenVR version and compatibility information - struct OpenVRInfo - { - bool isAvailable = false; - bool isCompatible = false; - std::string dllPath; - std::string version; - uint64_t fileSize = 0; - std::string modificationTime; - - // Interface probing results - bool hasOverlayInterface = false; - bool hasSystemInterface = false; - bool hasCompositorInterface = false; - - // Detection metadata - VRDetection::RuntimeType runtimeType = VRDetection::RuntimeType::Unknown; - bool probingSucceeded = false; - } openVRInfo; - - RE::NiPoint3 savedPlayerWorldPos = RE::NiPoint3(); // Used for auto-reset distance check - - // Wand pointing state - struct WandIntersectionState - { - bool isIntersecting = false; - ImVec2 uvCoordinates = ImVec2(0.0f, 0.0f); - vr::TrackedDeviceIndex_t controllerIndex = vr::k_unTrackedDeviceIndexInvalid; - Vector3 rayOrigin = Vector3::Zero; - Vector3 rayDirection = Vector3::Zero; - } wandState; - - // In-Scene Overlay Rendering Resources (Fallback for incompatible runtimes) - struct InSceneResources - { - winrt::com_ptr vs; - winrt::com_ptr ps; - winrt::com_ptr vb; - winrt::com_ptr ib; - winrt::com_ptr cb; - winrt::com_ptr inputLayout; - winrt::com_ptr blendState; - winrt::com_ptr depthState; - winrt::com_ptr sampler; - winrt::com_ptr rasterizerState; - - // Cached SRV to avoid creating every frame - winrt::com_ptr menuSRV; - ID3D11Texture2D* cachedMenuTexture = nullptr; - - // Cached RTVs per eye to avoid creating every frame - struct CachedRTV - { - winrt::com_ptr rtv; - ID3D11Texture2D* texture = nullptr; - }; - CachedRTV cachedEyeRTVs[2]; - - bool initialized = false; - } inSceneResources; - - struct InSceneCB - { - Matrix wvp; - }; - - void InitInSceneResources(); - void RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds); - void InstallSubmitHook(); - void DetectOpenVRInfo(); - bool IsOpenVRCompatible() const; - -private: - //============================================================================= - // PRIVATE HELPERS - //============================================================================= - - bool GetGripPressed(bool isLeft, bool isRight) const; - void ResetComboRecording(); - void ApplyRecordedCombo(); -}; diff --git a/src/Features/VR/InSceneOverlay.cpp b/src/Features/VR/InSceneOverlay.cpp deleted file mode 100644 index 30f04e9367..0000000000 --- a/src/Features/VR/InSceneOverlay.cpp +++ /dev/null @@ -1,612 +0,0 @@ -#include "Features/VR.h" -#include "Globals.h" -#include "Hooks.h" -#include "Menu.h" -#include "Util.h" -#include "Utils/VRUtils.h" -#include -#include -#include -#include -#include -#include - -using namespace DirectX; -using namespace DirectX::SimpleMath; - -using AttachMode = VR::Settings::OverlayAttachMode; - -//============================================================================= -// IN-SCENE OVERLAY RENDERING VIA SUBMIT HOOK -//============================================================================= - -namespace -{ - struct IVRCompositor_Submit - { - static vr::EVRCompositorError thunk(vr::IVRCompositor* _this, vr::EVREye eEye, const vr::Texture_t* pTexture, const vr::VRTextureBounds_t* pBounds, vr::EVRSubmitFlags nSubmitFlags) - { - auto& vr = globals::features::vr; - // Only process DirectX textures - skip OpenGL/Vulkan to avoid undefined behavior - if (pTexture && pTexture->handle && pTexture->eType == vr::TextureType_DirectX) { - vr.RenderInSceneOverlay(eEye, (ID3D11Texture2D*)pTexture->handle, pBounds); - } - return func(_this, eEye, pTexture, pBounds, nSubmitFlags); - } - static inline REL::Relocation func; - }; -} - -void VR::InitInSceneResources() -{ - if (inSceneResources.initialized) - return; - - InSceneResources temp = {}; - - auto device = globals::d3d::device; - - // 1. Compile shaders - compile VS to get bytecode for input layout, PS separately - ID3DBlob* vsBlob = nullptr; - ID3DBlob* psBlob = nullptr; - ID3DBlob* errorBlob = nullptr; - - // Compile vertex shader - if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.vs.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "main", "vs_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob))) { - if (errorBlob) { - logger::error("VR InScene VS compile error: {}", (char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - return; - } - if (errorBlob) { - errorBlob->Release(); - errorBlob = nullptr; - } - - // Compile pixel shader - if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.ps.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, - "main", "ps_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &psBlob, &errorBlob))) { - if (errorBlob) { - logger::error("VR InScene PS compile error: {}", (char*)errorBlob->GetBufferPointer()); - errorBlob->Release(); - } - if (vsBlob) - vsBlob->Release(); - return; - } - if (errorBlob) { - errorBlob->Release(); - errorBlob = nullptr; - } - - // Create shader objects from bytecode - ID3D11VertexShader* vs = nullptr; - ID3D11PixelShader* ps = nullptr; - if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &vs)) || - FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &ps))) { - logger::error("VR: Failed to create shader objects"); - if (vs) - vs->Release(); - if (ps) - ps->Release(); - if (vsBlob) - vsBlob->Release(); - if (psBlob) - psBlob->Release(); - return; - } - - temp.vs.attach(vs); - temp.ps.attach(ps); - if (psBlob) - psBlob->Release(); // Don't need PS blob anymore - - // 2. Input Layout - D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; - polygonLayout[0].SemanticName = "POSITION"; - polygonLayout[0].SemanticIndex = 0; - polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; - polygonLayout[0].InputSlot = 0; - polygonLayout[0].AlignedByteOffset = 0; - polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; - polygonLayout[0].InstanceDataStepRate = 0; - - polygonLayout[1].SemanticName = "TEXCOORD"; - polygonLayout[1].SemanticIndex = 0; - polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; - polygonLayout[1].InputSlot = 0; - polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; - polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; - polygonLayout[1].InstanceDataStepRate = 0; - - if (FAILED(device->CreateInputLayout(polygonLayout, 2, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), temp.inputLayout.put()))) { - logger::error("VR: Failed to create input layout"); - vsBlob->Release(); - return; - } - - vsBlob->Release(); - - // 3. Buffers - // Quad Vertices (XY plane, z=0, size=1) - struct VertexType - { - XMFLOAT3 position; - XMFLOAT2 texture; - }; - VertexType vertices[4] = { - { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT2(0.0f, 1.0f) }, // Bottom Left - { XMFLOAT3(-0.5f, 0.5f, 0.0f), XMFLOAT2(0.0f, 0.0f) }, // Top Left - { XMFLOAT3(0.5f, 0.5f, 0.0f), XMFLOAT2(1.0f, 0.0f) }, // Top Right - { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT2(1.0f, 1.0f) } // Bottom Right - }; - - D3D11_BUFFER_DESC vertexBufferDesc = {}; - vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; - vertexBufferDesc.ByteWidth = sizeof(VertexType) * 4; - vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; - D3D11_SUBRESOURCE_DATA vertexData = {}; - vertexData.pSysMem = vertices; - if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, temp.vb.put()))) { - logger::error("VR: Failed to create vertex buffer"); - return; - } - - unsigned long indices[6] = { 0, 1, 2, 0, 2, 3 }; - D3D11_BUFFER_DESC indexBufferDesc = {}; - indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; - indexBufferDesc.ByteWidth = sizeof(unsigned long) * 6; - indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; - D3D11_SUBRESOURCE_DATA indexData = {}; - indexData.pSysMem = indices; - if (FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, temp.ib.put()))) { - logger::error("VR: Failed to create index buffer"); - return; - } - - D3D11_BUFFER_DESC matrixBufferDesc = {}; - matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; - matrixBufferDesc.ByteWidth = sizeof(InSceneCB); - matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - if (FAILED(device->CreateBuffer(&matrixBufferDesc, nullptr, temp.cb.put()))) { - logger::error("VR: Failed to create constant buffer"); - return; - } - - // 4. States - D3D11_BLEND_DESC blendDesc = {}; - blendDesc.RenderTarget[0].BlendEnable = TRUE; - blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; - blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; - blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; - blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; - blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; - blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; - blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F; - if (FAILED(device->CreateBlendState(&blendDesc, temp.blendState.put()))) { - logger::error("VR: Failed to create blend state"); - return; - } - - D3D11_DEPTH_STENCIL_DESC depthDesc = {}; - depthDesc.DepthEnable = FALSE; // Always on top - depthDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; - depthDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; - if (FAILED(device->CreateDepthStencilState(&depthDesc, temp.depthState.put()))) { - logger::error("VR: Failed to create depth stencil state"); - return; - } - - D3D11_RASTERIZER_DESC rasterDesc = {}; - rasterDesc.FillMode = D3D11_FILL_SOLID; - rasterDesc.CullMode = D3D11_CULL_NONE; - rasterDesc.FrontCounterClockwise = FALSE; - rasterDesc.DepthClipEnable = TRUE; - if (FAILED(device->CreateRasterizerState(&rasterDesc, temp.rasterizerState.put()))) { - logger::error("VR: Failed to create rasterizer state"); - return; - } - - D3D11_SAMPLER_DESC samplerDesc = {}; - samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; - samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; - samplerDesc.MinLOD = 0; - samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; - if (FAILED(device->CreateSamplerState(&samplerDesc, temp.sampler.put()))) { - logger::error("VR: Failed to create sampler state"); - return; - } - Util::SetResourceName(temp.sampler.get(), "VR::InSceneOverlaySampler"); - - inSceneResources = std::move(temp); - inSceneResources.initialized = true; - logger::debug("VR: In-Scene Overlay resources initialized."); -} - -void VR::RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds) -{ - if (!globals::menu || !(globals::menu->IsEnabled || globals::menu->overlayVisible || IsWelcomeOverlayVisible()) || settings.attachMode == AttachMode::None || !menuTexture) { - return; - } - - auto context = globals::d3d::context; - winrt::com_ptr perf; - context->QueryInterface(__uuidof(ID3DUserDefinedAnnotation), perf.put_void()); - - static const wchar_t* eventNames[] = { L"VR In-Scene Overlay (Eye 0)", L"VR In-Scene Overlay (Eye 1)" }; - if (perf) - perf->BeginEvent(eventNames[(int)eye]); - - if (!inSceneResources.initialized) - InitInSceneResources(); - if (!inSceneResources.initialized) { - if (perf) - perf->EndEvent(); - return; - } - - // We can't render if we don't have HMD pose - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) { - if (perf) - perf->EndEvent(); - return; - } - - // Get HMD Pose and Eye matrices - vr::TrackedDevicePose_t hmdPose; - vr::TrackedDevicePose_t renderPose[vr::k_unMaxTrackedDeviceCount]; - - RE::BSOpenVR::GetIVRCompositor()->GetLastPoses(renderPose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - hmdPose = renderPose[vr::k_unTrackedDeviceIndex_Hmd]; - if (!hmdPose.bPoseIsValid) { - if (perf) - perf->EndEvent(); - return; - } - - Matrix hmdWorld = Matrix::Identity; - Matrix eyeToHead = Matrix::Identity; - Matrix proj = Matrix::Identity; - Matrix vpHeadSpace = Matrix::Identity; // For HMD-relative rendering (head space) - Matrix vpWorldSpace = Matrix::Identity; // For world/controller rendering (world space) - - // Always get Eye and Projection matrices - eyeToHead = Util::HmdMatrix34ToMatrix(openvr->vrSystem->GetEyeToHeadTransform(eye)); - - // Use GetProjectionRaw to build a DirectX-compatible projection matrix (Depth [0, 1]) - // IMPORTANT: OpenVR GetProjectionRaw has a known bug (Valve issue #110, open since 2016): - // The 3rd parameter (named "pTop") actually returns the BOTTOM tangent, and - // the 4th parameter (named "pBottom") actually returns the TOP tangent. - // We name our variables to match the ACTUAL values, not the misleading parameter names. - float left, right, bottom, top; - openvr->vrSystem->GetProjectionRaw(eye, &left, &right, &bottom, &top); - float nearZ = 0.1f; - float farZ = 1000.0f; - - proj = DirectX::XMMatrixPerspectiveOffCenterRH(left * nearZ, right * nearZ, bottom * nearZ, top * nearZ, nearZ, farZ); - - // Log projection values once per eye - static bool projLogged[2] = { false, false }; - if (!projLogged[(int)eye]) { - logger::debug("VR Projection Eye {}: L={:.4f} R={:.4f} B={:.4f} T={:.4f}, EyeX={:.4f}", - (int)eye, left, right, bottom, top, eyeToHead._41); - projLogged[(int)eye] = true; - } - - // Head-space VP (for HMD-relative mode) - vpHeadSpace = eyeToHead.Invert() * proj; - - // World-space VP (for controller attach and fixed world position modes) - if (hmdPose.bPoseIsValid) { - hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - // Transform chain: eye → head → world (row-vector: left-to-right composition) - Matrix eyeToWorld = eyeToHead * hmdWorld; - vpWorldSpace = eyeToWorld.Invert() * proj; - } - - // Get or create cached RTV for the target texture - D3D11_TEXTURE2D_DESC texDesc; - targetTexture->GetDesc(&texDesc); - - int eyeIdx = (int)eye; - auto& cachedRTV = inSceneResources.cachedEyeRTVs[eyeIdx]; - if (cachedRTV.texture != targetTexture) { - cachedRTV.rtv = nullptr; - cachedRTV.texture = nullptr; - - D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; - rtvDesc.Format = texDesc.Format; - - if (texDesc.ArraySize > 1) { - if (texDesc.SampleDesc.Count > 1) { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY; - rtvDesc.Texture2DMSArray.FirstArraySlice = (UINT)eye; - rtvDesc.Texture2DMSArray.ArraySize = 1; - } else { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; - rtvDesc.Texture2DArray.FirstArraySlice = (UINT)eye; - rtvDesc.Texture2DArray.ArraySize = 1; - rtvDesc.Texture2DArray.MipSlice = 0; - } - } else if (texDesc.SampleDesc.Count > 1) { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS; - } else { - rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; - rtvDesc.Texture2D.MipSlice = 0; - } - - HRESULT hr = globals::d3d::device->CreateRenderTargetView(targetTexture, &rtvDesc, cachedRTV.rtv.put()); - if (FAILED(hr)) { - logger::error("VR: Failed to create RTV for eye texture (Format: {}, Samples: {}). HRESULT: {:x}", - (uint32_t)texDesc.Format, texDesc.SampleDesc.Count, (uint32_t)hr); - if (perf) - perf->EndEvent(); - return; - } - cachedRTV.texture = targetTexture; - } - - auto& rtv = cachedRTV.rtv; - - // Save State - ID3D11RenderTargetView* oldRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT]; - ID3D11DepthStencilView* oldDSV; - context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, &oldDSV); - - D3D11_VIEWPORT oldViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; - UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; - context->RSGetViewports(&numViewports, oldViewports); - - ID3D11RasterizerState* oldRS = nullptr; - context->RSGetState(&oldRS); - - ID3D11BlendState* oldBlend = nullptr; - FLOAT oldBlendFactor[4]; - UINT oldSampleMask; - context->OMGetBlendState(&oldBlend, oldBlendFactor, &oldSampleMask); - - ID3D11DepthStencilState* oldDepth = nullptr; - UINT oldStencilRef; - context->OMGetDepthStencilState(&oldDepth, &oldStencilRef); - - // Setup Render - ID3D11RenderTargetView* rtvPtr = rtv.get(); - context->OMSetRenderTargets(1, &rtvPtr, nullptr); // No DSV - - // Viewport: Use bounds if provided (for SBS textures), otherwise use full texture - D3D11_VIEWPORT vpDesc = {}; - if (bounds) { - vpDesc.TopLeftX = bounds->uMin * texDesc.Width; - vpDesc.TopLeftY = bounds->vMin * texDesc.Height; - vpDesc.Width = (bounds->uMax - bounds->uMin) * texDesc.Width; - vpDesc.Height = (bounds->vMax - bounds->vMin) * texDesc.Height; - } else { - vpDesc.TopLeftX = 0.0f; - vpDesc.TopLeftY = 0.0f; - vpDesc.Width = (float)texDesc.Width; - vpDesc.Height = (float)texDesc.Height; - } - vpDesc.MinDepth = 0.0f; - vpDesc.MaxDepth = 1.0f; - context->RSSetViewports(1, &vpDesc); - - // Log texture and viewport details once per eye per session - static bool textureInfoLogged[2] = { false, false }; - if (!textureInfoLogged[eyeIdx]) { - logger::debug("VR Submit Texture Info (Eye {}):", eyeIdx); - logger::debug(" Texture Size: {}x{}, Format: {}, ArraySize: {}, SampleCount: {}", - texDesc.Width, texDesc.Height, (uint32_t)texDesc.Format, texDesc.ArraySize, texDesc.SampleDesc.Count); - if (bounds) { - logger::debug(" Bounds: uMin={:.3f}, vMin={:.3f}, uMax={:.3f}, vMax={:.3f}", - bounds->uMin, bounds->vMin, bounds->uMax, bounds->vMax); - logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", - vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); - } else { - logger::debug(" No bounds provided (full texture per eye, or texture array)"); - logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", - vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); - } - logger::debug(" RTV Dimension: {}", - (texDesc.ArraySize > 1 && texDesc.SampleDesc.Count > 1) ? "Texture2DMSArray" : - (texDesc.ArraySize > 1) ? "Texture2DArray (per-eye slice)" : - (texDesc.SampleDesc.Count > 1) ? "Texture2DMS" : - "Texture2D (single)"); - textureInfoLogged[eyeIdx] = true; - } - - // Helper to draw the overlay quad with a given WVP matrix - auto drawOverlayQuad = [&](ID3D11DeviceContext* ctx, const InSceneCB& cbData) { - D3D11_MAPPED_SUBRESOURCE mappedResource; - if (SUCCEEDED(ctx->Map(inSceneResources.cb.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource))) { - memcpy(mappedResource.pData, &cbData, sizeof(InSceneCB)); - ctx->Unmap(inSceneResources.cb.get(), 0); - } - - ctx->VSSetShader(inSceneResources.vs.get(), nullptr, 0); - ctx->PSSetShader(inSceneResources.ps.get(), nullptr, 0); - ID3D11Buffer* cb = inSceneResources.cb.get(); - ctx->VSSetConstantBuffers(0, 1, &cb); - - struct VT - { - XMFLOAT3 p; - XMFLOAT2 t; - }; - UINT stride = sizeof(VT); - UINT offset = 0; - ID3D11Buffer* vb = inSceneResources.vb.get(); - ctx->IASetVertexBuffers(0, 1, &vb, &stride, &offset); - ctx->IASetIndexBuffer(inSceneResources.ib.get(), DXGI_FORMAT_R32_UINT, 0); - ctx->IASetInputLayout(inSceneResources.inputLayout.get()); - ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - ctx->OMSetBlendState(inSceneResources.blendState.get(), nullptr, 0xFFFFFFFF); - ctx->OMSetDepthStencilState(inSceneResources.depthState.get(), 0); - ctx->RSSetState(inSceneResources.rasterizerState.get()); - - // Cache SRV to avoid creating every frame - if (menuTexture.get() != inSceneResources.cachedMenuTexture) { - inSceneResources.menuSRV = nullptr; - if (FAILED(globals::d3d::device->CreateShaderResourceView(menuTexture.get(), nullptr, inSceneResources.menuSRV.put()))) { - logger::error("VR: Failed to create menu texture SRV"); - return; - } - inSceneResources.cachedMenuTexture = menuTexture.get(); - } - ID3D11ShaderResourceView* srvPtr = inSceneResources.menuSRV.get(); - ctx->PSSetShaderResources(0, 1, &srvPtr); - - ID3D11SamplerState* sampler = inSceneResources.sampler.get(); - ctx->PSSetSamplers(0, 1, &sampler); - - ctx->DrawIndexed(6, 0, 0); - }; - - // --- Render HMD Overlay --- - if ((settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) && menuTexture) { - InSceneCB cbData; - - Matrix modelMatrix; - Matrix vp; - if (settings.VRMenuPositioningMethod == 1) { // Fixed World Position - modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * fixedWorldOverlayPosition.m; - vp = vpWorldSpace; - } else { // HMD Relative - Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); - modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * offset; - vp = vpHeadSpace; - } - cbData.wvp = (modelMatrix * vp).Transpose(); - - drawOverlayQuad(context, cbData); - } - - // --- Render Controller Overlay --- - if ((settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) && menuTexture) { - vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachIndex != vr::k_unTrackedDeviceIndexInvalid && attachIndex < vr::k_unMaxTrackedDeviceCount) { - vr::TrackedDevicePose_t controllerPose = renderPose[attachIndex]; - if (controllerPose.bPoseIsValid) { - Matrix controllerWorld = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); - Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); - Matrix modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * offset * controllerWorld; - - // Backface culling: hide overlay when viewed from behind - // Use the unscaled controller+offset transform for correct normal direction - Matrix overlayTransform = offset * controllerWorld; - Vector3 overlayNormal(overlayTransform._31, overlayTransform._32, overlayTransform._33); - overlayNormal.Normalize(); - Matrix eyeWorld = eyeToHead * hmdWorld; - Vector3 eyePos = eyeWorld.Translation(); - Vector3 overlayPos = overlayTransform.Translation(); - Vector3 toEye = eyePos - overlayPos; - toEye.Normalize(); - // Quad front face is +Z in local space (D3D default CW winding). - // Render when eye is on the +Z side of the overlay (dot > 0). - float dot = overlayNormal.Dot(toEye); - if (dot > 0.0f) { - InSceneCB cbData; - cbData.wvp = (modelMatrix * vpWorldSpace).Transpose(); - drawOverlayQuad(context, cbData); - } - } - } - } - - // Restore State - context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, oldDSV); - context->RSSetViewports(numViewports, oldViewports); - context->OMSetBlendState(oldBlend, oldBlendFactor, oldSampleMask); - context->OMSetDepthStencilState(oldDepth, oldStencilRef); - if (oldRS) { - context->RSSetState(oldRS); - oldRS->Release(); - } - if (oldBlend) - oldBlend->Release(); - if (oldDepth) - oldDepth->Release(); - for (int i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) - if (oldRTVs[i]) - oldRTVs[i]->Release(); - if (oldDSV) - oldDSV->Release(); - - if (perf) - perf->EndEvent(); -} - -void VR::InstallSubmitHook() -{ - static bool installed = false; - if (installed) - return; - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (openvr && RE::BSOpenVR::GetIVRCompositor()) { - logger::info("VR: Installing IVRCompositor::Submit hook for in-scene overlay rendering"); - - // Log comprehensive VR system parameters (debug only) - logger::debug("=== VR System Configuration ==="); - - // Get and log IPD - float ipd = Util::GetIPDFromHMD(); - logger::debug("IPD: {:.4f} meters ({:.2f} mm)", ipd, ipd * 1000.0f); - - // Get and log eye transforms - if (openvr->vrSystem) { - vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); - vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); - - logger::debug("Left Eye Transform:"); - logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", - leftEye.m[0][3], leftEye.m[1][3], leftEye.m[2][3]); - logger::debug("Right Eye Transform:"); - logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", - rightEye.m[0][3], rightEye.m[1][3], rightEye.m[2][3]); - logger::debug("Calculated Eye Separation: {:.4f} meters ({:.2f} mm)", - std::abs(leftEye.m[0][3] - rightEye.m[0][3]), - std::abs(leftEye.m[0][3] - rightEye.m[0][3]) * 1000.0f); - - // Get projection matrices - vr::HmdMatrix44_t leftProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); - vr::HmdMatrix44_t rightProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Right, 0.1f, 1000.0f); - - logger::debug("Projection Matrices (near=0.1, far=1000.0):"); - logger::debug(" Left [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", - leftProj.m[0][0], leftProj.m[1][1], leftProj.m[0][2]); - logger::debug(" Right [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", - rightProj.m[0][0], rightProj.m[1][1], rightProj.m[0][2]); - } - - logger::debug("Convergence Formula Info:"); - logger::debug(" Formula: stereoShift = (IPD/2) / (depth * tan(hFOV/2))"); - logger::debug(" - Shift is independent of scale (scale only controls size)"); - logger::debug(" - Depth is controlled by OffsetZ (negative = in front)"); - float halfIPD = ipd / 2.0f; - if (openvr->vrSystem) { - vr::HmdMatrix44_t proj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); - float tanHFOV = 1.0f / proj.m[0][0]; - logger::debug(" tan(hFOV/2) = {:.4f}", tanHFOV); - logger::debug(" Example: At depth 1.0m, shift={:.6f}", halfIPD / (1.0f * tanHFOV)); - logger::debug(" Example: At depth 2.0m, shift={:.6f}", halfIPD / (2.0f * tanHFOV)); - logger::debug(" Example: At depth 5.0m, shift={:.6f}", halfIPD / (5.0f * tanHFOV)); - } - logger::debug("================================"); - - // IVRCompositor::Submit is index 5 - stl::detour_vfunc<5, IVRCompositor_Submit>(RE::BSOpenVR::GetIVRCompositor()); - installed = true; - - logger::info("VR: In-scene overlay initialized"); - } else { - logger::warn("VR: Failed to install IVRCompositor::Submit hook - Interface not available"); - } -} diff --git a/src/Features/VR/Input.cpp b/src/Features/VR/Input.cpp deleted file mode 100644 index b4c2278115..0000000000 --- a/src/Features/VR/Input.cpp +++ /dev/null @@ -1,383 +0,0 @@ -#include "Features/VR.h" -#include "Menu.h" -#include "State.h" -#include "Utils/PerfUtils.h" -#include "Utils/VRUtils.h" - -#include - -using AttachMode = VR::Settings::OverlayAttachMode; - -void VR::UpdateOverlayMenuStateFromInput() -{ - if (this->isCapturingCombo) { - return; - } - - if (globals::menu == nullptr) - return; - - bool& isEnabled = globals::menu->IsEnabled; - bool& overlayEnabled = globals::menu->overlayVisible; - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - - if (testMode) { - if (!isEnabled) { - settings.VRMenuControllerDiagnosticsTestMode = false; - return; - } - return; - } - - bool uiMenusOpen = globals::state->isMainMenuOpen || - (globals::game::ui && globals::game::ui->IsMenuOpen(RE::TweenMenu::MENU_NAME)); - - bool inValidMenuState = uiMenusOpen || (globals::game::ui && (isEnabled || overlayEnabled)); - - if (!inValidMenuState) - return; - - struct MenuStateMapping - { - std::function condition; - std::function action; - bool allowWhenUIMenusClosed = false; - }; - - auto CheckCombo = [&](const std::vector& combos) -> bool { - if (combos.empty()) - return false; - - for (size_t i = 0; i < combos.size(); ++i) { - const auto& combo = combos[i]; - bool buttonPressed = false; - - switch (combo.GetDevice()) { - case ControllerDevice::Both: - buttonPressed = primaryControllerState[combo.GetKey()].isPressed && - secondaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Primary: - buttonPressed = primaryControllerState[combo.GetKey()].isPressed; - break; - case ControllerDevice::Secondary: - buttonPressed = secondaryControllerState[combo.GetKey()].isPressed; - break; - } - - if (!buttonPressed) { - return false; - } - } - - return true; - }; - - std::vector mappings = { - // Open Community Shaders menu when closed - { [&]() { - return CheckCombo(settings.VRMenuOpenKeys) && !isEnabled; - }, - [&]() { isEnabled = true; } }, - - // Close Community Shaders menu when open - { [&]() { - return CheckCombo(settings.VRMenuCloseKeys) && isEnabled; - }, - [&]() { - isEnabled = false; - overlayDragState.dragging = false; - }, - true }, - - // Open VR overlay when closed (only when CS menu is open) - { [&]() { - return CheckCombo(settings.VROverlayOpenKeys) && !overlayEnabled && isEnabled; - }, - [&]() { overlayEnabled = true; } }, - - // Close VR overlay when open (only when CS menu is open) - { [&]() { - return CheckCombo(settings.VROverlayCloseKeys) && overlayEnabled && isEnabled; - }, - [&]() { overlayEnabled = false; } } - }; - - bool onlyAllowClose = isEnabled && !uiMenusOpen; - - for (const auto& mapping : mappings) { - if (onlyAllowClose && !mapping.allowWhenUIMenusClosed) - continue; - - if (mapping.condition()) { - mapping.action(); - break; - } - } -} - -void VR::ProcessVREvents(std::vector& vrEvents) -{ - bool currentLeftHandedMode = RE::BSOpenVRControllerDevice::IsLeftHandedMode(); - static bool firstCall = true; - if (firstCall || currentLeftHandedMode != lastKnownLeftHandedMode) { - if (!firstCall) { - logger::debug("VR handedness changed: {} -> {}", lastKnownLeftHandedMode ? "Left" : "Right", currentLeftHandedMode ? "Left" : "Right"); - } - firstCall = false; - lastKnownLeftHandedMode = currentLeftHandedMode; - primaryControllerState = {}; - secondaryControllerState = {}; - } - - double nowSecs = Util::GetNowSecs(); - for (auto& event : vrEvents) { - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - struct VRButtonDescriptor - { - const char* name; - bool (*isButton)(std::uint32_t); - std::uint32_t keyCode; - }; - static const VRButtonDescriptor kVRButtons[] = { - { "Grip", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGrip }, - { "GripAlt", RE::BSOpenVRControllerDevice::IsGripButton, RE::BSOpenVRControllerDevice::Keys::kGripAlt }, - { "Trigger", RE::BSOpenVRControllerDevice::IsTriggerButton, RE::BSOpenVRControllerDevice::Keys::kTrigger }, - { "Stick Click", RE::BSOpenVRControllerDevice::IsStickClick, RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger }, - { "Touchpad Click", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadClick }, - { "Touchpad Alt", RE::BSOpenVRControllerDevice::IsTouchpadClick, RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt }, - { "A/X", RE::BSOpenVRControllerDevice::IsAButton, RE::BSOpenVRControllerDevice::Keys::kXA }, - { "B/Y", RE::BSOpenVRControllerDevice::IsBButton, RE::BSOpenVRControllerDevice::Keys::kBY }, - }; - for (const auto& desc : kVRButtons) { - if (event.keyCode == desc.keyCode) { - RE::ButtonState* state = isPrimary ? &primaryControllerState[desc.keyCode] : isSecondary ? &secondaryControllerState[desc.keyCode] : - nullptr; - if (state) { - state->OnEvent(event.IsPressed(), nowSecs); - } - break; - } - } - switch (event.eventType) { - case RE::INPUT_EVENT_TYPE::kButton: - ProcessVRButtonEvent(event); - break; - case RE::INPUT_EVENT_TYPE::kThumbstick: - UpdateControllerState(event); - break; - default: - break; - } - } -} - -void VR::ProcessVRButtonEvent(const Menu::KeyEvent& event) -{ - if (this->isCapturingCombo) { - return; - } - - ImGuiIO& io = ImGui::GetIO(); - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - bool& testMode = settings.VRMenuControllerDiagnosticsTestMode; - constexpr size_t kNumTriggerMappings = 1; - - if (isPrimary || isSecondary) { - constexpr size_t kNumMappings = 6; - RE::ButtonMapping mappings[kNumMappings] = { - { RE::BSOpenVRControllerDevice::Keys::kTrigger, ImGuiMouseButton_Left, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kGrip, ImGuiMouseButton_Right, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kTouchpadClick, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger, ImGuiMouseButton_Middle, false, ImGuiKey_None, false }, - { RE::BSOpenVRControllerDevice::Keys::kBY, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_TAB), isSecondary }, - { RE::BSOpenVRControllerDevice::Keys::kXA, -1, true, Util::Input::VirtualKeyToImGuiKey(VK_RETURN), false }, - }; - - static bool prevPrimaryStates[kNumMappings] = {}; - static bool prevSecondaryStates[kNumMappings] = {}; - static bool lastHandedness = false; - if (lastHandedness != lastKnownLeftHandedMode) { - memset(prevPrimaryStates, 0, sizeof(prevPrimaryStates)); - memset(prevSecondaryStates, 0, sizeof(prevSecondaryStates)); - lastHandedness = lastKnownLeftHandedMode; - } - bool* prevStates = isPrimary ? prevPrimaryStates : prevSecondaryStates; - - RE::InputDeviceState& controllerState = isPrimary ? primaryControllerState : secondaryControllerState; - - size_t limit = testMode ? kNumTriggerMappings : kNumMappings; - - for (size_t i = 0; i < limit; ++i) { - RE::ButtonState* state = &controllerState[mappings[i].keyCode]; - bool curr = state ? state->isPressed : false; - if (curr != prevStates[i]) { - if (mappings[i].isKeyEvent) { - if (mappings[i].isShift) - io.AddKeyEvent(ImGuiMod_Shift, curr); - io.AddKeyEvent(static_cast(mappings[i].key), curr); - } else { - io.AddMouseButtonEvent(mappings[i].logicalButton, curr); - } - prevStates[i] = curr; - } - } - } - - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "button"; - logEntry.thumbstickX = 0.0f; - logEntry.thumbstickY = 0.0f; - logEntry.controllerRole = isPrimary ? "Primary" : isSecondary ? "Secondary" : - "Unknown"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -void VR::UpdateControllerState(const Menu::KeyEvent& event) -{ - bool isPrimary = RE::BSOpenVRControllerDevice::IsPrimaryController(event.device); - bool isSecondary = RE::BSOpenVRControllerDevice::IsSecondaryController(event.device); - - if (isPrimary) { - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].x = event.thumbstickX; - primaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Primary)].y = event.thumbstickY; - } else if (isSecondary) { - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].x = event.thumbstickX; - secondaryControllerState.thumbsticks[static_cast(RE::ControllerRole::Secondary)].y = event.thumbstickY; - } - - VRControllerEventLog logEntry; - logEntry.device = static_cast(event.device); - logEntry.keyCode = event.keyCode; - logEntry.value = static_cast(event.value); - logEntry.pressed = event.IsPressed(); - logEntry.heldTime = 0.0; - logEntry.heldSource = "thumbstick"; - logEntry.thumbstickX = event.thumbstickX; - logEntry.thumbstickY = event.thumbstickY; - logEntry.controllerRole = isPrimary ? "Primary" : "Secondary"; - vrControllerEventLog.push_back(logEntry); - if (vrControllerEventLog.size() > 32) { - vrControllerEventLog.erase(vrControllerEventLog.begin()); - } -} - -void VR::ProcessThumbstickScroll(RE::VRControllerState& controllerState, size_t thumbstickIndex, float deadzone, ImGuiIO& io) -{ - bool usingScrollStickX = (std::abs(controllerState.thumbsticks[thumbstickIndex].x) > deadzone); - bool usingScrollStickY = (std::abs(controllerState.thumbsticks[thumbstickIndex].y) > deadzone); - - if (usingScrollStickX || usingScrollStickY) { - struct ScrollAccum - { - float x = 0.0f; - float y = 0.0f; - }; - static std::unordered_map scrollAccums; - ScrollAccum& accum = scrollAccums[thumbstickIndex]; - - accum.x += controllerState.thumbsticks[thumbstickIndex].x * 0.1f; - accum.y += controllerState.thumbsticks[thumbstickIndex].y * 0.1f; - - float scrollEventX = 0.0f; - float scrollEventY = 0.0f; - - if (std::abs(accum.x) > 0.3f) { - scrollEventX = accum.x > 0 ? 1.0f : -1.0f; - accum.x = 0.0f; - } - if (std::abs(accum.y) > 0.3f) { - scrollEventY = accum.y > 0 ? 1.0f : -1.0f; - accum.y = 0.0f; - } - - if (scrollEventX != 0.0f || scrollEventY != 0.0f) { - io.AddMouseWheelEvent(-scrollEventX, scrollEventY); - } - } -} - -void VR::ProcessControllerInputForImGui() -{ - if (!globals::menu || !globals::menu->IsEnabled) - return; - bool testMode = settings.VRMenuControllerDiagnosticsTestMode; - float mouseDeadzone = settings.mouseDeadzone; - float mouseSpeed = settings.mouseSpeed; - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; - io.WantSetMousePos = false; - - bool wandHandledCursor = false; - if (!testMode && settings.EnableWandPointing) { - UpdateCursorFromWandPointing(); - wandHandledCursor = wandState.isIntersecting; - } - - if (!testMode) { - bool isDragging = overlayDragState.dragging; - - if (wandHandledCursor && !isDragging) { - ProcessThumbstickScroll(primaryControllerState, static_cast(RE::ControllerRole::Primary), mouseDeadzone, io); - ProcessThumbstickScroll(secondaryControllerState, static_cast(RE::ControllerRole::Secondary), mouseDeadzone, io); - } else if (!isDragging) { - bool useAttachedControllerForCursor = (settings.attachMode == VR::Settings::OverlayAttachMode::ControllerOnly || - settings.attachMode == VR::Settings::OverlayAttachMode::Both); - - RE::VRControllerState* cursorController = nullptr; - RE::VRControllerState* scrollController = nullptr; - - if (useAttachedControllerForCursor) { - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } else { - cursorController = &secondaryControllerState; - scrollController = &primaryControllerState; - } - } else { - cursorController = &primaryControllerState; - scrollController = &secondaryControllerState; - } - - if (cursorController) { - size_t thumbstickIndex = (cursorController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - - float thumbstickX = cursorController->thumbsticks[thumbstickIndex].x; - float thumbstickY = cursorController->thumbsticks[thumbstickIndex].y; - bool usingCursorStick = (std::abs(thumbstickX) > mouseDeadzone || std::abs(thumbstickY) > mouseDeadzone); - - if (usingCursorStick) { - ImVec2 mousePos = io.MousePos; - mousePos.x += thumbstickX * mouseSpeed; - mousePos.y -= thumbstickY * mouseSpeed; - mousePos.x = std::clamp(mousePos.x, 0.0f, io.DisplaySize.x); - mousePos.y = std::clamp(mousePos.y, 0.0f, io.DisplaySize.y); - io.MousePos = mousePos; - io.AddMousePosEvent(mousePos.x, mousePos.y); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - } - } - - if (scrollController) { - size_t thumbstickIndex = (scrollController == &primaryControllerState) ? - static_cast(RE::ControllerRole::Primary) : - static_cast(RE::ControllerRole::Secondary); - ProcessThumbstickScroll(*scrollController, thumbstickIndex, mouseDeadzone, io); - } - } - } -} diff --git a/src/Features/VR/OpenVRDetection.cpp b/src/Features/VR/OpenVRDetection.cpp deleted file mode 100644 index 78223d28b9..0000000000 --- a/src/Features/VR/OpenVRDetection.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#include "OpenVRDetection.h" -#include -#include -#include -#include -#include -#pragma comment(lib, "version.lib") - -namespace VRDetection -{ - const char* RuntimeTypeToString(RuntimeType type) - { - switch (type) { - case RuntimeType::SteamVR: - return "SteamVR"; - case RuntimeType::OpenComposite: - return "OpenComposite"; - default: - return "Unknown"; - } - } - - bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result) - { - HMODULE hModule = GetModuleHandleA("openvr_api.dll"); - if (!hModule) - return false; - - using pfnIsValid = bool(__cdecl*)(const char*); - auto IsValid = reinterpret_cast(GetProcAddress(hModule, "VR_IsInterfaceVersionValid")); - if (!IsValid) - return false; - - result.hasOverlayInterface = IsValid(vr::IVROverlay_Version); - result.hasSystemInterface = IsValid(vr::IVRSystem_Version); - result.hasCompositorInterface = IsValid(vr::IVRCompositor_Version); - - result.probingSucceeded = result.hasOverlayInterface && result.hasSystemInterface && result.hasCompositorInterface; - return result.probingSucceeded; - } - - void GatherDLLInfo(OpenVRDetectionResult& result) - { - HMODULE hModule = GetModuleHandleA("openvr_api.dll"); - if (!hModule) { - result.isAvailable = false; - return; - } - - result.isAvailable = true; - - char dllPath[MAX_PATH]; - DWORD fileLength = GetModuleFileNameA(hModule, dllPath, MAX_PATH); - if (fileLength == 0 || (fileLength == MAX_PATH && GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { - result.isAvailable = false; - return; - } - - result.dllPath = dllPath; - - DWORD dwSize = GetFileVersionInfoSizeA(dllPath, nullptr); - if (dwSize > 0) { - std::vector buffer(dwSize); - if (GetFileVersionInfoA(dllPath, 0, dwSize, buffer.data())) { - VS_FIXEDFILEINFO* pFileInfo = nullptr; - UINT len = 0; - if (VerQueryValueA(buffer.data(), "\\", reinterpret_cast(&pFileInfo), &len)) { - DWORD major = HIWORD(pFileInfo->dwFileVersionMS); - DWORD minor = LOWORD(pFileInfo->dwFileVersionMS); - DWORD build = HIWORD(pFileInfo->dwFileVersionLS); - DWORD revision = LOWORD(pFileInfo->dwFileVersionLS); - result.version = std::format("{}.{}.{}.{}", major, minor, build, revision); - } - } - } - - if (result.version.empty()) - result.version = "Unknown"; - - WIN32_FIND_DATAA findData; - HANDLE hFind = FindFirstFileA(dllPath, &findData); - if (hFind != INVALID_HANDLE_VALUE) { - FindClose(hFind); - ULARGE_INTEGER fileSize; - fileSize.LowPart = findData.nFileSizeLow; - fileSize.HighPart = findData.nFileSizeHigh; - result.fileSize = fileSize.QuadPart; - - SYSTEMTIME st; - FileTimeToSystemTime(&findData.ftLastWriteTime, &st); - result.modificationTime = std::format("{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); - } - } - - RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize) - { - // OpenComposite DLLs are typically small (~600KB) with version 1.0.10.0 - if (version == "1.0.10.0" && fileSize < 700000) - return RuntimeType::OpenComposite; - - // Check path for OpenComposite indicators - std::string lowerPath = dllPath; - for (auto& c : lowerPath) - c = static_cast(std::tolower(static_cast(c))); - - if (lowerPath.find("opencomposite") != std::string::npos) - return RuntimeType::OpenComposite; - - // SteamVR DLLs are typically larger and have higher version numbers - if (lowerPath.find("steamvr") != std::string::npos || lowerPath.find("steam") != std::string::npos) - return RuntimeType::SteamVR; - - // Higher version numbers suggest SteamVR - if (!version.empty() && version != "Unknown" && version != "1.0.10.0") - return RuntimeType::SteamVR; - - return RuntimeType::Unknown; - } - - OpenVRDetectionResult Detect() - { - OpenVRDetectionResult result; - - GatherDLLInfo(result); - if (!result.isAvailable) - return result; - - result.runtimeType = DetectRuntimeType(result.dllPath, result.version, result.fileSize); - - // Detect compatibility via runtime interface probing - result.isCompatible = ProbeRuntimeInterfaces(result); - - return result; - } -} diff --git a/src/Features/VR/OpenVRDetection.h b/src/Features/VR/OpenVRDetection.h deleted file mode 100644 index 1eaa411272..0000000000 --- a/src/Features/VR/OpenVRDetection.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once -#include -#include - -namespace VRDetection -{ - enum class RuntimeType - { - Unknown, - SteamVR, - OpenComposite - }; - - struct OpenVRDetectionResult - { - bool isAvailable = false; - bool isCompatible = false; - - // Interface probing results - bool hasOverlayInterface = false; - bool hasSystemInterface = false; - bool hasCompositorInterface = false; - - // File-based info - std::string dllPath; - std::string version; - uint64_t fileSize = 0; - std::string modificationTime; - - // Detection metadata - RuntimeType runtimeType = RuntimeType::Unknown; - bool probingSucceeded = false; - }; - - // Runtime interface probing via VR_IsInterfaceVersionValid - bool ProbeRuntimeInterfaces(OpenVRDetectionResult& result); - - // Gather DLL metadata (path, version, size, timestamp) - void GatherDLLInfo(OpenVRDetectionResult& result); - - // Detect runtime type (SteamVR vs OpenComposite) - RuntimeType DetectRuntimeType(const std::string& dllPath, const std::string& version, uint64_t fileSize); - - // Full detection via interface probing - OpenVRDetectionResult Detect(); - - const char* RuntimeTypeToString(RuntimeType type); -} diff --git a/src/Features/VR/OverlayDrag.cpp b/src/Features/VR/OverlayDrag.cpp deleted file mode 100644 index bd0180b33c..0000000000 --- a/src/Features/VR/OverlayDrag.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include "Features/VR.h" -#include "RE/B/BSOpenVR.h" -#include "RE/P/PlayerCharacter.h" -#include "Utils/VRUtils.h" - -#include -#include -#include -#include -#include - -using namespace DirectX::SimpleMath; -using AttachMode = VR::Settings::OverlayAttachMode; - -bool VR::GetGripPressed(bool isLeft, bool isRight) const -{ - bool isLeftHanded = lastKnownLeftHandedMode; - - if (isLeft) { - if (isLeftHanded) { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - if (isRight) { - if (isLeftHanded) { - return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } else { - return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; - } - } - return false; -} - -static bool CanStartAny(vr::ETrackedControllerRole role) -{ - return role != vr::TrackedControllerRole_Invalid; -} - -void VR::UpdateOverlayDrag() -{ - if (!CanPerformDrag()) { - return; - } - - if (overlayDragState.dragging) { - UpdateActiveDrag(); - } else { - TryStartNewDrag(); - } -} - -bool VR::CanPerformDrag() -{ - if (!settings.EnableDragToReposition) - return false; - - if (!globals::menu || !globals::menu->IsEnabled) - return false; - - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return false; - - if (settings.VRMenuControllerDiagnosticsTestMode) { - return false; - } - - return true; -} - -void VR::UpdateActiveDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - auto resetDragState = [&]() { - overlayDragState.dragging = false; - overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; - overlayDragState.isPrimary = false; - overlayDragState.isSecondary = false; - }; - - float rawMatrix[3][4]; - if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - - switch (overlayDragState.mode) { - case OverlayDragState::DragMode::Controller: - { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - float attachedM[3][4]; - if (!Util::GetControllerWorldMatrix(attachedControllerIndex, attachedM)) - break; - { - Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachedM)); - - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - - Matrix worldToLocal = attachedControllerMatrix.Invert(); - Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); - - settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; - settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; - settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; - } - } - break; - } - case OverlayDragState::DragMode::FixedWorld: - { - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - Matrix translated = overlayDragState.initialOverlayMatrix; - translated._41 += worldDelta.x; - translated._42 += worldDelta.y; - translated._43 += worldDelta.z; - fixedWorldOverlayPosition.m = translated; - break; - } - case OverlayDragState::DragMode::HMD: - { - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - break; - if (hmdPose.bPoseIsValid) { - Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - - Vector3 worldDelta( - controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, - controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, - controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); - - Matrix worldToLocal = hmdMatrix.Invert(); - Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); - - static auto lastDeltaLog = std::chrono::steady_clock::now(); - auto nowDelta = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(nowDelta - lastDeltaLog).count() > 500) { - logger::debug("VR Drag Delta - Local: ({:.3f}, {:.3f}, {:.3f})", localDelta.x, localDelta.y, localDelta.z); - lastDeltaLog = nowDelta; - } - - settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; - settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; - settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; - settings.VRMenuScale = overlayDragState.initialHMDScale; - - static std::chrono::steady_clock::time_point lastLog = std::chrono::steady_clock::now(); - auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastLog).count() > 500) { - logger::debug("VR Dragging (3D Mode): Offset ({:.2f}, {:.2f}, {:.2f}), Scale {:.2f}", - settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ, settings.VRMenuScale); - lastLog = now; - } - } - break; - } - default: - break; - } - } - - // Joystick depth control during grip - if (overlayDragState.dragging) { - RE::VRControllerState* gripController = nullptr; - size_t thumbIdx = 0; - if (overlayDragState.isPrimary) { - if (lastKnownLeftHandedMode) { - gripController = &primaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Primary); - } else { - gripController = &secondaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Secondary); - } - } else if (overlayDragState.isSecondary) { - if (lastKnownLeftHandedMode) { - gripController = &secondaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Secondary); - } else { - gripController = &primaryControllerState; - thumbIdx = static_cast(RE::ControllerRole::Primary); - } - } - - if (gripController) { - float thumbY = gripController->thumbsticks[thumbIdx].y; - const float deadzone = settings.mouseDeadzone; - const float depthSpeed = 0.02f; - if (std::abs(thumbY) > deadzone) { - float depthDelta = -thumbY * depthSpeed; - if (overlayDragState.mode == OverlayDragState::DragMode::HMD) { - overlayDragState.initialHMDOffset.z += depthDelta; - overlayDragState.initialHMDOffset.z = std::clamp(overlayDragState.initialHMDOffset.z, -10.0f, 10.0f); - } else if (overlayDragState.mode == OverlayDragState::DragMode::Controller) { - overlayDragState.initialControllerOffset.z += depthDelta; - overlayDragState.initialControllerOffset.z = std::clamp(overlayDragState.initialControllerOffset.z, -10.0f, 10.0f); - } - } - } - } - - bool gripPressed = GetGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); - if (!gripPressed) { - resetDragState(); - } -} - -void VR::TryStartNewDrag() -{ - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - auto* system = openvr ? openvr->vrSystem : nullptr; - if (!system) - return; - - struct DragMode - { - OverlayDragState::DragMode mode; - bool isActive; - std::function canStart; - std::function onInit; - }; - - std::vector dragModes; - - // Controller mode - only for opposite hand (highest priority) - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - dragModes.push_back({ OverlayDragState::DragMode::Controller, - true, - [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); - if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); - if (deviceRole == role && i == oppositeControllerIndex) - return true; - } - } - return false; - }, - [&]() { - overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; - overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; - overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - // Fixed world mode - if (settings.VRMenuPositioningMethod == 1) { - std::function fixedWorldCanStart; - if (settings.attachMode == AttachMode::Both) { - fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - return role == actualAttachedRole; - } - return false; - }; - } else { - fixedWorldCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, - true, - fixedWorldCanStart, - [&]() { - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; - } }); - } - - // HMD mode - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - std::function hmdCanStart; - if (settings.attachMode == AttachMode::Both) { - hmdCanStart = [&](vr::ETrackedControllerRole role) { - vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { - vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); - return role == actualAttachedRole; - } - return false; - }; - } else { - hmdCanStart = CanStartAny; - } - - dragModes.push_back({ OverlayDragState::DragMode::HMD, - true, - hmdCanStart, - [&]() { - overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; - overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; - overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; - overlayDragState.initialHMDScale = settings.VRMenuScale; - overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; - } }); - } - - for (const auto& mode : dragModes) { - if (!mode.isActive) - continue; - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) - continue; - vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); - bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); - bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (!mode.canStart(role)) - continue; - bool gripPressed = GetGripPressed(isLeft, isRight); - if (!gripPressed) - continue; - float rawMatrix[3][4]; - if (!Util::GetControllerWorldMatrix(i, rawMatrix)) - continue; - vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); - Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); - overlayDragState.dragging = true; - overlayDragState.mode = mode.mode; - overlayDragState.controllerIndex = i; - overlayDragState.isPrimary = isLeft; - overlayDragState.isSecondary = isRight; - overlayDragState.startControllerMatrix = controllerMatrix; - mode.onInit(); - - if (system && globals::menu->IsEnabled) { - for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { - if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { - vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); - bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); - if (isRightController == isRight) { - openvr->TriggerHapticPulse(isRightController, 25.0f); - break; - } - } - } - } - - return; - } - } -} - -void VR::SetFixedOverlayToCurrentHMD() -{ - vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( - settings.VRMenuOffsetX, - settings.VRMenuOffsetY, - settings.VRMenuOffsetZ); - fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); -} - -void VR::UpdateFixedWorldPositioning() -{ - if (settings.VRMenuPositioningMethod != 1) - return; - - if (!fixedWorldOverlayPosition.initialized) { - fixedWorldOverlayPosition.initialized = true; - SetFixedOverlayToCurrentHMD(); - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) { - savedPlayerWorldPos = player->GetPosition(); - } - return; - } - - if (settings.VRMenuAutoResetDistance > 0.0f) { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) { - RE::NiPoint3 playerPos = player->GetPosition(); - float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); - float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; - if (sqDist > thresholdSq) { - SetFixedOverlayToCurrentHMD(); - savedPlayerWorldPos = playerPos; - } - } - } -} diff --git a/src/Features/VR/SettingsUI.cpp b/src/Features/VR/SettingsUI.cpp deleted file mode 100644 index 08819d2084..0000000000 --- a/src/Features/VR/SettingsUI.cpp +++ /dev/null @@ -1,1139 +0,0 @@ -#include "FeatureConstraints.h" -#include "Features/DynamicCubemaps.h" -#include "Features/ScreenSpaceGI.h" -#include "Features/ScreenSpaceShadows.h" -#include "Features/VR.h" -#include "Menu.h" -#include "Menu/Fonts.h" -#include "RE/B/BSOpenVR.h" -#include "RE/P/PlayerCharacter.h" -#include "State.h" -#include "Utils/PerfUtils.h" -#include "Utils/UI.h" -#include "Utils/VRUtils.h" - -#include - -using AttachMode = VR::Settings::OverlayAttachMode; - -namespace -{ - bool BeginTabItemWithFont(const char* label, Menu::FontRole role, ImGuiTabItemFlags flags = ImGuiTabItemFlags_None) - { - return MenuFonts::BeginTabItemWithFont(label, role, flags); - } -} - -//============================================================================= -// COMBO RECORDING HELPERS -//============================================================================= - -void VR::ResetComboRecording() -{ - isCapturingCombo = false; - currentComboType = ComboType::None; - currentComboName = nullptr; - recordedCombo.clear(); - comboStartTime = 0.0; - recordingButtonControllers.clear(); -} - -void VR::ApplyRecordedCombo() -{ - if (recordedCombo.empty()) - return; - - switch (currentComboType) { - case ComboType::MenuOpen: - settings.VRMenuOpenKeys = recordedCombo; - break; - case ComboType::MenuClose: - settings.VRMenuCloseKeys = recordedCombo; - break; - case ComboType::OverlayOpen: - settings.VROverlayOpenKeys = recordedCombo; - break; - case ComboType::OverlayClose: - settings.VROverlayCloseKeys = recordedCombo; - break; - default: - break; - } -} - -//============================================================================= -// OVERLAY (WELCOME MESSAGE) -//============================================================================= - -void VR::DrawOverlay() -{ - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - static LARGE_INTEGER overlayShowStart = { 0 }; - static LARGE_INTEGER freq = { 0 }; - - bool shouldShow = IsWelcomeOverlayVisible(); - - if (!shouldShow) { - overlayShowStart.QuadPart = 0; - return; - } - - if (freq.QuadPart == 0) { - QueryPerformanceFrequency(&freq); - } - - LARGE_INTEGER now; - QueryPerformanceCounter(&now); - - if (overlayShowStart.QuadPart == 0) { - overlayShowStart = now; - } - - double elapsed = double(now.QuadPart - overlayShowStart.QuadPart) / double(freq.QuadPart); - const double autoHideSeconds = static_cast(settings.kAutoHideSeconds); - if (elapsed >= autoHideSeconds) { - return; - } - int secondsLeft = int(std::ceil(autoHideSeconds - elapsed)); - - ImGuiIO& io = ImGui::GetIO(); - const float scale = Util::GetUIScale(); - ImVec2 overlaySize(520 * scale, 0); - ImVec2 overlayPos = ImVec2((io.DisplaySize.x - overlaySize.x) * 0.5f, (io.DisplaySize.y * 0.35f)); - ImGui::SetNextWindowPos(overlayPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(overlaySize, ImGuiCond_Always); - ImGui::SetNextWindowBgAlpha(0.95f); - - ImGui::Begin("HowToUseOverlay", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); - - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f * scale); - ImGui::TextWrapped("How to Use VR Community Shaders Menu:"); - ImGui::Separator(); - ImGui::TextWrapped("You must open the Main Menu or Tween Menu before VR controls work."); - ImGui::Spacing(); - ImGui::PopTextWrapPos(); - - ImGui::Text("Open Menu: "); - ImGui::SameLine(); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - - ImGui::Text("Close Menu: "); - ImGui::SameLine(); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - - ImGui::Spacing(); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 500.0f * scale); - ImGui::TextWrapped("Grip + Thumbstick: Adjust overlay depth (closer/farther)"); - ImGui::Spacing(); - ImGui::TextWrapped("Tip: Disable this VR overlay by setting Attach Mode to 'None' in VR settings."); - ImGui::Spacing(); - ImGui::TextWrapped("(This welcome message will auto-hide in %d seconds)", secondsLeft); - ImGui::TextWrapped("(Disable in: VR settings > Controller Input Instructions)"); - ImGui::PopTextWrapPos(); - - ImGui::End(); -} - -//============================================================================= -// ANONYMOUS NAMESPACE: SETTINGS PANEL DRAW FUNCTIONS -//============================================================================= - -namespace -{ - void DrawControllerInputInstructions() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.IsOpenVRCompatible()) - return; - if (ImGui::CollapsingHeader("Controller Input Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderInt("Auto-hide Welcome overlay timeout", &settings.kAutoHideSeconds, 0, VR::Config::kMaxAutoHideSeconds, - settings.kAutoHideSeconds <= 0 ? "Hidden" : "%d seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Set to 0 to hide the overlay, or a positive value to show it for that many seconds"); - } - ImGui::TextWrapped("Menu (while in the main menu or tween menu):"); - if (ImGui::BeginTable("MenuInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Community Shaders Menu:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VRMenuCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Overlay (while in the main menu or tween menu):"); - if (ImGui::BeginTable("OverlayInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Open Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayOpenKeys, true); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Close Overlay:"); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(settings.VROverlayCloseKeys, true); - ImGui::EndTable(); - } - ImGui::TextWrapped("Menu Controller Input:"); - if (ImGui::BeginTable("ControllerInputTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Trigger (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Left mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Grip (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Right mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Touchpad Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "Stick Click (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Middle mouse button"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerBothColor(), "A/X (Both Controllers)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Enter"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "B/Y (Primary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Tab"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "B/Y (Secondary Controller)"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Shift+Tab"); - ImGui::EndTable(); - } - bool useAttachedControllerForCursor = (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both); - if (ImGui::BeginTable("ThumbstickInstructionsTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - if (useAttachedControllerForCursor) { - if (settings.VRMenuAttachController == ControllerDevice::Primary) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (attached controller)"); - } - } else { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerPrimaryColor(), "Primary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Mouse movement (HMD mode)"); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextColored(Util::GetControllerSecondaryColor(), "Secondary Controller Thumbstick"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Scroll"); - } - ImGui::EndTable(); - } - } - } - - void DrawStereoSettings() - { - auto& vr = globals::features::vr; - VR::Settings& settings = vr.settings; - - if (ImGui::CollapsingHeader("Stereo Reprojection", ImGuiTreeNodeFlags_DefaultOpen)) - vr.stereoOpt.DrawSettings(); - - bool hasEffects = VR::AnyScreenSpaceEffectLoaded(); - bool isDev = globals::state && globals::state->IsDeveloperMode(); - - if (ImGui::CollapsingHeader("Stereo Blend", ImGuiTreeNodeFlags_DefaultOpen)) { - if (!hasEffects && !isDev) { - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Requires an active screen-space effect (SSGI, SS Shadows, SSR)."); - } else { - if (!hasEffects) - ImGui::TextColored(ImVec4(0.6f, 0.6f, 1.0f, 1.0f), "Developer mode: no screen-space effects active."); - - ImGui::Checkbox("Enable Stereo Blend", &settings.EnableStereoBlend); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Post-composite depth-aware bilateral blend between eyes.\n" - "Reduces stereo inconsistencies from screen-space effects (SSGI, SSR, etc.).\n" - "Each pixel is reprojected to the other eye; blending is applied only where\n" - "depth agrees (same surface). Full-screen pass in VR."); - } - - ImGui::BeginDisabled(!settings.EnableStereoBlend); - - ImGui::SliderFloat("Depth Sigma", &settings.StereoBlendDepthSigma, 0.001f, 0.1f, "%.4f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Depth sensitivity for the bilateral weight.\n" - "Lower values are stricter -- only blend when depths match very closely.\n" - "Higher values allow blending across slight depth differences.\n" - "Default: 0.01"); - } - - ImGui::SliderFloat("Max Blend Factor", &settings.StereoBlendMaxFactor, 0.0f, 0.5f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Maximum blend strength between the two eyes.\n" - "Higher values reduce screen-space effect flicker but destroy stereo depth.\n" - "Keep below ~0.15 to preserve 3D parallax.\n" - "Default: 0.1"); - } - - ImGui::SliderFloat("Color Difference Threshold", &settings.StereoBlendColorThreshold, 0.0f, 0.2f, "%.3f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Minimum luminance difference between eyes to trigger blending.\n" - "Set to 0 to blend everywhere. Higher = more selective.\n" - "Default: 0.02"); - } - - ImGui::EndDisabled(); - } - } - - if (hasEffects || isDev) { - ImGui::Separator(); - - // Auto-enable required feature when a debug mode is selected; restore on Off. - // Tracks what we toggled so user-initiated changes aren't clobbered. - static bool s_weEnabledStereoBlend = false; - static bool s_weEnabledReproj = false; - - const char* debugModes[] = { "Off", "Back-Check", "Blend Weight", "Edge Detection", "Overwrite", "Overwrite Eye1" }; - if (ImGui::Combo("Debug View", &settings.StereoBlendDebugMode, debugModes, IM_ARRAYSIZE(debugModes))) { - int newMode = settings.StereoBlendDebugMode; - bool needsBlend = (newMode >= 1 && newMode <= 3); - bool needsReproj = (newMode == 4 || newMode == 5); - - // Auto-enable Stereo Blend for modes 1-3 (runtime-toggleable) - if (needsBlend && !settings.EnableStereoBlend) { - settings.EnableStereoBlend = true; - s_weEnabledStereoBlend = true; - } else if (!needsBlend && s_weEnabledStereoBlend) { - settings.EnableStereoBlend = false; - s_weEnabledStereoBlend = false; - } - - // Auto-enable Reprojection for modes 4-5 (note: takes effect after restart) - auto& sm = vr.stereoOpt.settings.stereoMode; - if (needsReproj && sm == VRStereoOptimizations::StereoMode::Off) { - sm = VRStereoOptimizations::StereoMode::Enable; - s_weEnabledReproj = true; - } else if (!needsReproj && s_weEnabledReproj) { - sm = VRStereoOptimizations::StereoMode::Off; - s_weEnabledReproj = false; - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Selecting a debug mode auto-enables the required feature; setting back to Off restores it.\n\n" - "Off: Normal rendering\n" - "Back-Check: Round-trip reprojection validation (auto-enables Stereo Blend)\n" - "Blend Weight: Heatmap of bilateral blend intensity (auto-enables Stereo Blend)\n" - "Edge Detection: Highlights depth discontinuities (auto-enables Stereo Blend)\n" - "Overwrite: Mode texture classification (auto-enables Reprojection -- restart required)\n" - " Green=edge Pink=edge neighbour Blue=disoccluded Orange=full blend\n" - "Overwrite Eye1: POM depth heatmap for Eye 1 (auto-enables Reprojection -- restart required)"); - } - } - } - - void DrawGeneralVRSettings() - { - auto& vr = globals::features::vr; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("General Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - bool exteriorChanged = ImGui::Checkbox("Enable Depth Buffer Culling in Exteriors", &settings.EnableDepthBufferCullingExterior); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in exteriors, recommended ON."); - } - - bool interiorChanged = ImGui::Checkbox("Enable Depth Buffer Culling in Interiors", &settings.EnableDepthBufferCullingInterior); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Improves performance in interiors, recommended ON."); - } - - if (exteriorChanged || interiorChanged) { - vr.UpdateDepthBufferCulling(); - } - - if (ImGui::SliderFloat("Min Occludee Box Extent", &settings.MinOccludeeBoxExtent, 0.0f, 1000.0f, "%.1f")) { - if (vr.gMinOccludeeBoxExtent) { - *vr.gMinOccludeeBoxExtent = settings.MinOccludeeBoxExtent; - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Minimum bounding box dimensions for object occlusion culling. Lower values improve performance but may result in visual artifacts."); - } - } - } - - void DrawMenuSettings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - if (!vr.IsOpenVRCompatible()) - return; - if (ImGui::CollapsingHeader("Menu Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - float maxScale = VR::Config::kMaxMenuScale; - ImGui::SliderFloat("Menu Scale", &settings.VRMenuScale, VR::Config::kMinMenuScale, maxScale, "%.2f"); - const char* positioningMethods[] = { "HMD Relative", "Fixed World Position" }; - int prevMethod = settings.VRMenuPositioningMethod; - if (ImGui::Combo("Menu Positioning Method", &settings.VRMenuPositioningMethod, positioningMethods, IM_ARRAYSIZE(positioningMethods))) { - if (prevMethod != 1 && settings.VRMenuPositioningMethod == 1) { - vr.SetFixedOverlayToCurrentHMD(); - auto player = RE::PlayerCharacter::GetSingleton(); - if (player) - vr.savedPlayerWorldPos = player->GetPosition(); - } - } - const char* attachModes[] = { "HMD Only", "Controller Only", "Both", "None (Disabled)" }; - int attachModeInt = static_cast(settings.attachMode); - if (ImGui::Combo("Attach Mode", &attachModeInt, attachModes, IM_ARRAYSIZE(attachModes))) { - settings.attachMode = static_cast(attachModeInt); - } - - if (settings.attachMode == AttachMode::ControllerOnly || - settings.attachMode == AttachMode::Both) { - const char* attachControllers[] = { "Primary Controller", "Secondary Controller" }; - int attachControllerInt = static_cast(settings.VRMenuAttachController); - if (ImGui::Combo("Attach to Controller", &attachControllerInt, attachControllers, IM_ARRAYSIZE(attachControllers))) { - settings.VRMenuAttachController = static_cast(attachControllerInt); - } - - ImGui::Separator(); - ImGui::Text("Controller Offset Settings"); - ImGui::SliderFloat("Controller Offset X", &settings.VRMenuControllerOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Y", &settings.VRMenuControllerOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("Controller Offset Z", &settings.VRMenuControllerOffsetZ, -2.0f, 2.0f, "%.2f"); - } - - if (settings.attachMode == AttachMode::HMDOnly || - settings.attachMode == AttachMode::Both) { - ImGui::Separator(); - ImGui::Text("HMD Offset Settings"); - ImGui::SliderFloat("HMD Offset X", &settings.VRMenuOffsetX, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Y", &settings.VRMenuOffsetY, -2.0f, 2.0f, "%.2f"); - ImGui::SliderFloat("HMD Offset Z", &settings.VRMenuOffsetZ, -4.0f, 1.0f, "%.2f"); - } - - if (settings.VRMenuPositioningMethod == 1) { - ImGui::Separator(); - ImGui::Text("Fixed World Position Settings"); - ImGui::SliderFloat("Auto Reset Distance (game units)", &settings.VRMenuAutoResetDistance, 100.0f, 5000.0f, "%.0f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("If you move farther than this distance from the menu, it will automatically reset to your HMD position. %s", Util::Units::FormatDistance(settings.VRMenuAutoResetDistance).c_str()); - } - if (ImGui::Button("Reset Menu to HMD Position")) { - vr.SetFixedOverlayToCurrentHMD(); - } - } - } - } - - void DrawMouseSettings() - { - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Input Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Enable Wand Pointing", &settings.EnableWandPointing)) { - vr.wandState.isIntersecting = false; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Use controller ray-casting to point at UI elements"); - } - ImGui::Separator(); - ImGui::Text("Joystick Settings"); - ImGui::SliderFloat("Mouse Deadzone", &settings.mouseDeadzone, 0.0f, 1.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Thumbstick deadzone for joystick cursor movement"); - } - ImGui::SliderFloat("Mouse Speed", &settings.mouseSpeed, 0.1f, 50.0f, "%.2f"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Speed multiplier for joystick cursor movement"); - } - } - } - - void DrawDragSettings() - { - auto& vr = globals::features::vr; - if (!vr.IsOpenVRCompatible()) - return; - VR::Settings& settings = vr.settings; - if (ImGui::CollapsingHeader("Drag Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::CollapsingHeader("Drag Instructions", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextWrapped("Overlay Positioning (Grip + Drag):"); - ImGui::BulletText("Fixed World Position: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("HMD Relative: Any controller can drag (HMD-only mode) or attached controller only (Both modes)"); - ImGui::BulletText("Controller Attached: Only the opposite hand can drag the controller overlay"); - ImGui::Spacing(); - ImGui::TextWrapped("Depth Adjustment (Grip + Thumbstick):"); - ImGui::BulletText("While gripping to drag, use the thumbstick on the same hand to adjust depth"); - ImGui::BulletText("Thumbstick forward: Push overlay farther away"); - ImGui::BulletText("Thumbstick back: Pull overlay closer"); - } - ImGui::Checkbox("Enable drag to reposition overlays", &settings.EnableDragToReposition); - ImGui::BeginDisabled(!settings.EnableDragToReposition); - ImGui::ColorEdit4("Drag Highlight Color", settings.dragHighlightColor.data()); - ImGui::EndDisabled(); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Color used to highlight draggable overlays in VR."); - } - } - } - - void DrawKeyBindings() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - - if (ImGui::CollapsingHeader("Combo Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Combo Timeout", &settings.comboTimeout, 1.0f, 10.0f, "%.1f seconds"); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time limit for recording button combinations."); - } - } - ImGui::Separator(); - const char* comboTypes[] = { - "Open Community Shaders Menu", - "Close Community Shaders Menu", - "Open VR Overlay", - "Close VR Overlay" - }; - static int selectedComboIndex = 0; - ImGui::Text("Select Combo to Record:"); - ImGui::SameLine(); - if (ImGui::Combo("##ComboSelector", &selectedComboIndex, comboTypes, IM_ARRAYSIZE(comboTypes))) { - vr.isCapturingCombo = false; - vr.currentComboType = VR::ComboType::None; - vr.recordedCombo.clear(); - } - if (ImGui::Button("Record Selected Combo")) { - vr.isCapturingCombo = true; - vr.currentComboType = static_cast(selectedComboIndex + 1); - vr.currentComboName = comboTypes[selectedComboIndex]; - vr.recordedCombo.clear(); - vr.comboStartTime = Util::GetNowSecs(); - vr.recordingButtonControllers.clear(); - } - ImGui::SameLine(); - if (ImGui::SmallButton("Clear")) { - switch (selectedComboIndex) { - case 0: - settings.VRMenuOpenKeys.clear(); - break; - case 1: - settings.VRMenuCloseKeys.clear(); - break; - case 2: - settings.VROverlayOpenKeys.clear(); - break; - case 3: - settings.VROverlayCloseKeys.clear(); - break; - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Click to start recording a new button combination for the selected action."); - } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - if (ImGui::BeginTable("##VRBindingsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Action"); - ImGui::TableSetupColumn("Current Binding"); - ImGui::TableSetupColumn("Description"); - ImGui::TableHeadersRow(); - struct VRKeyBindingConfig - { - const char* label; - std::vector& combos; - const char* description; - const char* controllerRequirement; - }; - std::vector keyBindingConfigs = { - { "Open Community Shaders Menu", settings.VRMenuOpenKeys, "Button combination to open the Community Shaders menu", "Primary" }, - { "Close Community Shaders Menu", settings.VRMenuCloseKeys, "Button combination to close the Community Shaders menu", "Both" }, - { "Open VR Overlay", settings.VROverlayOpenKeys, "Button combination to open the VR overlay", "Primary" }, - { "Close VR Overlay", settings.VROverlayCloseKeys, "Button combination to close the VR overlay", "Secondary" } - }; - for (size_t row = 0; row < keyBindingConfigs.size(); ++row) { - const auto& config = keyBindingConfigs[row]; - ImGui::TableNextRow(); - if (row == static_cast(selectedComboIndex)) { - ImU32 highlight = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 0.15f)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, highlight); - } - ImGui::TableSetColumnIndex(0); - char selectableId[64]; - snprintf(selectableId, sizeof(selectableId), "##combo_row_%zu", row); - bool rowSelected = (row == static_cast(selectedComboIndex)); - if (ImGui::Selectable(selectableId, rowSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, 0))) { - selectedComboIndex = static_cast(row); - } - ImGui::SameLine(0, 0); - ImGui::Text("%s", config.label); - ImGui::TableSetColumnIndex(1); - Util::DrawButtonCombo(config.combos, false); - ImGui::TableSetColumnIndex(2); - ImGui::Text("%s", config.description); - } - ImGui::EndTable(); - } - ImGui::Spacing(); - if (ImGui::Button("Reset to Defaults")) { - VR::Settings defaults; - settings.VRMenuOpenKeys = defaults.VRMenuOpenKeys; - settings.VRMenuCloseKeys = defaults.VRMenuCloseKeys; - settings.VROverlayOpenKeys = defaults.VROverlayOpenKeys; - settings.VROverlayCloseKeys = defaults.VROverlayCloseKeys; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Reset all VR key bindings to their default values."); - } - } - - void DrawThumbstickColumn(VR& vr, bool showPrimary, ImU32 highlightCol) - { - auto& state = showPrimary ? vr.primaryControllerState : vr.secondaryControllerState; - auto role = showPrimary ? RE::ControllerRole::Primary : RE::ControllerRole::Secondary; - float x = state.thumbsticks[static_cast(role)].x; - float y = state.thumbsticks[static_cast(role)].y; - - ImVec2 padSize = ImVec2(80, 80); - ImVec2 cursor = ImGui::GetCursorScreenPos(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - ImVec2 center = ImVec2(cursor.x + padSize.x / 2, cursor.y + padSize.y / 2); - float radius = padSize.x / 2 - 4; - ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border); - ImU32 axisCol = ImGui::GetColorU32(ImGuiCol_TextDisabled); - ImU32 dotCol = ImGui::GetColorU32(ImGuiCol_Text); - - drawList->AddRectFilled(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), ImGui::GetColorU32(ImGuiCol_FrameBg)); - drawList->AddRect(cursor, ImVec2(cursor.x + padSize.x, cursor.y + padSize.y), borderCol, 4.0f, 0, 2.0f); - drawList->AddLine(ImVec2(center.x, cursor.y + 4), ImVec2(center.x, cursor.y + padSize.y - 4), axisCol, 1.0f); - drawList->AddLine(ImVec2(cursor.x + 4, center.y), ImVec2(cursor.x + padSize.x - 4, center.y), axisCol, 1.0f); - - int quad = 0; - if (x > 0 && y > 0) - quad = 1; - else if (x < 0 && y > 0) - quad = 2; - else if (x < 0 && y < 0) - quad = 3; - else if (x > 0 && y < 0) - quad = 4; - - if (quad != 0) { - ImVec2 q0 = center, q1 = center, q2 = center, q3 = center; - if (quad == 1) { - q1 = { center.x + radius, center.y - radius }; - q2 = { center.x + radius, center.y }; - q3 = { center.x, center.y - radius }; - } else if (quad == 2) { - q1 = { center.x - radius, center.y - radius }; - q2 = { center.x - radius, center.y }; - q3 = { center.x, center.y - radius }; - } else if (quad == 3) { - q1 = { center.x - radius, center.y + radius }; - q2 = { center.x - radius, center.y }; - q3 = { center.x, center.y + radius }; - } else if (quad == 4) { - q1 = { center.x + radius, center.y + radius }; - q2 = { center.x + radius, center.y }; - q3 = { center.x, center.y + radius }; - } - ImVec2 poly[4] = { center, q1, q2, q3 }; - drawList->AddConvexPolyFilled(poly, 4, highlightCol); - } - - ImVec2 dot = ImVec2(center.x + x * radius, center.y - y * radius); - drawList->AddCircleFilled(dot, 5.0f, dotCol); - - ImGui::Dummy(padSize); - ImGui::SetNextItemWidth(160.0f); - ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetTextLineHeight()); - ImGui::Text("X: %+1.3f Y: %+1.3f [%s]", x, y, RE::GetQuadrantName(x, y)); - } - - void DrawDebugSection() - { - auto& vr = globals::features::vr; - auto& settings = vr.settings; - auto menu = globals::menu; - - if (ImGui::CollapsingHeader("OpenVR Information", ImGuiTreeNodeFlags_DefaultOpen)) { - auto& info = vr.openVRInfo; - if (info.isAvailable) { - if (vr.IsOpenVRCompatible()) { - ImGui::Text("OpenVR System: Active & Compatible"); - } else { - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "OpenVR System: Active but INCOMPATIBLE"); - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f), "VR overlay menus disabled."); - } - - ImGui::Text("Runtime: %s", VRDetection::RuntimeTypeToString(info.runtimeType)); - ImGui::Text("DLL Path: %s", info.dllPath.c_str()); - ImGui::Text("DLL Version: %s", info.version.c_str()); - ImGui::Text("DLL Size: %llu bytes", info.fileSize); - ImGui::Text("Modified: %s", info.modificationTime.c_str()); - - ImGui::Separator(); - ImGui::Text("Detection Method:"); - ImGui::Text(" Interface Probing: %s", info.probingSucceeded ? "Passed" : "Failed"); - ImGui::Text(" IVROverlay_016: %s", info.hasOverlayInterface ? "OK" : "Missing"); - ImGui::Text(" IVRSystem_017: %s", info.hasSystemInterface ? "OK" : "Missing"); - ImGui::Text(" IVRCompositor_021: %s", info.hasCompositorInterface ? "OK" : "Missing"); - ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), " Rendering: In-scene overlay (submit hook)"); - - } else { - ImGui::Text("OpenVR system not available"); - } - } - - if (ImGui::CollapsingHeader("Controller Diagnostics", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Test Mode: Disable controller menu input (except scroll controller and triggers)", &settings.VRMenuControllerDiagnosticsTestMode)) { - ImGui::SetScrollHereY(0.0f); - } - ImGui::SeparatorText("Button State"); - double nowSecs = Util::GetNowSecs(); - ImVec4 highlightColor = menu->GetTheme().StatusPalette.InfoColor; - ImU32 highlightColorU32 = ImGui::ColorConvertFloat4ToU32(highlightColor); - - bool isLeftHanded = vr.lastKnownLeftHandedMode; - - if (ImGui::BeginTable("vr_input_state_table", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Button"); - if (isLeftHanded) { - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - } else { - ImGui::TableSetupColumn("Secondary State"); - ImGui::TableSetupColumn("Secondary Held (s)"); - ImGui::TableSetupColumn("Secondary Type"); - ImGui::TableSetupColumn("Primary State"); - ImGui::TableSetupColumn("Primary Held (s)"); - ImGui::TableSetupColumn("Primary Type"); - } - ImGui::TableHeadersRow(); - - auto DrawButtonType = [](const RE::ButtonState& state) { - if (!state.isPressed) { - if (state.IsClick()) - ImGui::TextUnformatted("Click"); - else if (state.IsHold()) - ImGui::TextUnformatted("Hold"); - else - ImGui::TextUnformatted("-"); - } else { - ImGui::TextUnformatted("Held"); - } - }; - - auto printRow = [&](const char* label, const RE::ButtonState& left, const RE::ButtonState& right) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(label); - ImGui::TableSetColumnIndex(1); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(left.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(2); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", left.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(3); - if (left.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(left); - ImGui::TableSetColumnIndex(4); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::TextUnformatted(right.isPressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(5); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - ImGui::Text("%.2f", right.GetCurrentHeldTime(nowSecs)); - ImGui::TableSetColumnIndex(6); - if (right.isPressed) - ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, highlightColorU32); - DrawButtonType(right); - }; - - auto printRowWithHandedness = [&](const char* label, auto key) { - auto& primary = vr.primaryControllerState[key]; - auto& secondary = vr.secondaryControllerState[key]; - if (isLeftHanded) { - printRow(label, primary, secondary); - } else { - printRow(label, secondary, primary); - } - }; - - printRowWithHandedness("Trigger", RE::BSOpenVRControllerDevice::Keys::kTrigger); - printRowWithHandedness("Grip", RE::BSOpenVRControllerDevice::Keys::kGrip); - printRowWithHandedness("GripAlt", RE::BSOpenVRControllerDevice::Keys::kGripAlt); - printRowWithHandedness("Stick Click", RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger); - printRowWithHandedness("Touchpad Click", RE::BSOpenVRControllerDevice::Keys::kTouchpadClick); - printRowWithHandedness("Touchpad Alt", RE::BSOpenVRControllerDevice::Keys::kTouchpadAlt); - printRowWithHandedness("B/Y", RE::BSOpenVRControllerDevice::Keys::kBY); - printRowWithHandedness("A/X", RE::BSOpenVRControllerDevice::Keys::kXA); - ImGui::EndTable(); - } - - ImGui::SeparatorText("VR Thumbstick State"); - ImU32 highlightCol = ImGui::ColorConvertFloat4ToU32(menu->GetTheme().StatusPalette.InfoColor); - if (ImGui::BeginTable("##VRThumbstickTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - if (isLeftHanded) { - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } else { - ImGui::TableSetupColumn("Secondary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Primary Controller", ImGuiTableColumnFlags_WidthFixed, 200.0f); - } - ImGui::TableHeadersRow(); - - // Left column - ImGui::TableSetColumnIndex(0); - ImGui::BeginGroup(); - DrawThumbstickColumn(vr, isLeftHanded, highlightCol); - ImGui::EndGroup(); - - // Right column - ImGui::TableSetColumnIndex(1); - ImGui::BeginGroup(); - DrawThumbstickColumn(vr, !isLeftHanded, highlightCol); - ImGui::EndGroup(); - ImGui::EndTable(); - } - - ImGui::SeparatorText("Recent VR Controller Events"); - ImGui::TextDisabled("Note: For thumbstick events, KeyCode/Value columns show X/Y floats."); - if (ImGui::BeginTable("eventlog", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { - ImGui::TableSetupColumn("Device", ImGuiTableColumnFlags_WidthFixed, 60.0f); - ImGui::TableSetupColumn("KeyCode/X", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Value/Y", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableSetupColumn("Pressed", ImGuiTableColumnFlags_WidthFixed, 70.0f); - ImGui::TableSetupColumn("Known Mapping", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableSetupColumn("Event Type", ImGuiTableColumnFlags_WidthFixed, 120.0f); - ImGui::TableHeadersRow(); - for (const auto& e : vr.vrControllerEventLog) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%d", e.device); - ImGui::TableSetColumnIndex(1); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickX); - } else { - ImGui::Text("%d", e.keyCode); - } - ImGui::TableSetColumnIndex(2); - if (e.heldSource == "thumbstick") { - ImGui::Text("%.3f", e.thumbstickY); - } else { - ImGui::Text("%d", e.value); - } - ImGui::TableSetColumnIndex(3); - ImGui::Text("%s", e.pressed ? "Pressed" : "Released"); - ImGui::TableSetColumnIndex(4); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted(e.controllerRole.c_str()); - } else { - ImGui::TextUnformatted(RE::GetOpenVRButtonName(e.keyCode)); - } - ImGui::TableSetColumnIndex(5); - if (e.heldSource == "thumbstick") { - ImGui::TextUnformatted("-"); - } else { - if (!e.pressed) { - if (e.heldTime > 0.0) { - if (e.heldTime < 0.5) { - ImGui::Text("Click (%.2fs)", e.heldTime); - } else { - ImGui::Text("Hold (%.2fs)", e.heldTime); - } - } else { - ImGui::Text("Release"); - } - } else if (e.pressed) { - if (e.heldTime > 0.0) { - ImGui::Text("Held for %.2fs", e.heldTime); - } else { - ImGui::Text("Press"); - } - } - } - } - ImGui::EndTable(); - } - - ImGui::SeparatorText("Wand Pointing State"); - if (ImGui::BeginTable("##WandPointingState", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableHeadersRow(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Wand Pointing Enabled"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%s", settings.EnableWandPointing ? "Yes" : "No"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Intersecting Overlay"); - ImGui::TableSetColumnIndex(1); - if (vr.wandState.isIntersecting) { - ImGui::TextColored(menu->GetTheme().StatusPalette.InfoColor, "YES"); - } else { - ImGui::Text("No"); - } - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("UV Coordinates"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.3f, %.3f)", vr.wandState.uvCoordinates.x, vr.wandState.uvCoordinates.y); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Controller Index"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%u", vr.wandState.controllerIndex); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Origin"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayOrigin.x, vr.wandState.rayOrigin.y, vr.wandState.rayOrigin.z); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Ray Direction"); - ImGui::TableSetColumnIndex(1); - ImGui::Text("(%.2f, %.2f, %.2f)", vr.wandState.rayDirection.x, vr.wandState.rayDirection.y, vr.wandState.rayDirection.z); - - ImGui::EndTable(); - } - } - - if (ImGui::CollapsingHeader("OpenVR Addresses")) { - auto openvr = RE::BSOpenVR::GetSingleton(); - auto overlay = openvr ? RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext) : nullptr; - auto vrSystem = openvr ? openvr->vrSystem : nullptr; - ADDRESS_NODE(openvr) - ADDRESS_NODE(overlay) - ADDRESS_NODE(vrSystem) - } - } -} // namespace - -//============================================================================= -// DRAW SETTINGS (main entry point) -//============================================================================= - -void VR::DrawSettings() -{ - auto menu = globals::menu; - if (!menu) - return; - if (ImGui::BeginTabBar("##VRTabs", ImGuiTabBarFlags_None)) { - if (BeginTabItemWithFont("General", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRGeneralFrame", { 0, 0 }, true)) { - DrawGeneralVRSettings(); - DrawControllerInputInstructions(); - DrawMenuSettings(); - DrawMouseSettings(); - DrawDragSettings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - if (BeginTabItemWithFont("Stereo", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRStereoFrame", { 0, 0 }, true)) { - DrawStereoSettings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - if (IsOpenVRCompatible()) { - if (BeginTabItemWithFont("Bindings", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRBindingsFrame", { 0, 0 }, true)) { - DrawKeyBindings(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - } - - if (BeginTabItemWithFont("Debug", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##VRDebugFrame", { 0, 0 }, true)) { - DrawDebugSection(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - // Combo recording popup - if (this->isCapturingCombo) { - ImGui::OpenPopup("Record Combo"); - if (auto popup = Util::CenteredPopupModal("Record Combo")) { - auto GetButtonName = [](uint32_t key) -> const char* { - switch (key) { - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTrigger): - return "Trigger"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kGrip): - return "Grip"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kTouchpadClick): - return "Touchpad"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kJoystickTrigger): - return "Stick Click"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kXA): - return "A/X"; - case static_cast(RE::BSOpenVRControllerDevice::Keys::kBY): - return "B/Y"; - default: - return "Unknown"; - } - }; - - ImGui::Text("Recording combo for: %s", this->currentComboName ? this->currentComboName : "Unknown"); - ImGui::Spacing(); - - ImGui::TextDisabled("(During recording, any controller's buttons can be used. Requirement is only enforced during use.)"); - - ImGui::Spacing(); - - double remainingTime = settings.comboTimeout - (Util::GetNowSecs() - this->comboStartTime); - ImVec4 timerColor = remainingTime > 2.0 ? Util::Colors::GetTimerGood() : - remainingTime > 1.0 ? Util::Colors::GetTimerWarning() : - Util::Colors::GetTimerCritical(); - ImGui::TextColored(timerColor, "Time remaining: %.1f seconds", remainingTime); - - ImGui::Spacing(); - - if (this->recordedCombo.empty()) { - ImGui::Text("Press buttons to record combo..."); - } else { - ImGui::Text("Recorded buttons:"); - std::vector sortedRecordedCombos; - for (size_t i = 0; i < this->recordedCombo.size(); ++i) { - sortedRecordedCombos.push_back(this->recordedCombo[i]); - } - std::sort(sortedRecordedCombos.begin(), sortedRecordedCombos.end(), - [](const ButtonCombo& a, const ButtonCombo& b) { - return a.GetKey() < b.GetKey(); - }); - - Util::DrawButtonCombo(sortedRecordedCombos, false); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::Text("Press ENTER to accept, ESC to cancel"); - - // Handle button recording - bool buttonPressed = false; - uint32_t pressedKey = 0; - ControllerDevice pressedDevice = ControllerDevice::Both; - - for (const auto& [keyCode, buttonState] : primaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Primary; - break; - } - } - - if (!buttonPressed) { - for (const auto& [keyCode, buttonState] : secondaryControllerState.GetActiveButtons()) { - if (buttonState->isPressed) { - pressedKey = keyCode; - buttonPressed = true; - pressedDevice = ControllerDevice::Secondary; - break; - } - } - } - - if (buttonPressed) { - auto it = recordingButtonControllers.find(pressedKey); - if (it == recordingButtonControllers.end()) { - recordingButtonControllers[pressedKey] = pressedDevice; - } else { - if (it->second != pressedDevice && it->second != ControllerDevice::Both) { - it->second = ControllerDevice::Both; - } - } - this->recordedCombo.clear(); - for (const auto& [key, device] : recordingButtonControllers) { - this->recordedCombo.push_back(ButtonCombo(device, key)); - } - } - - if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) { - ApplyRecordedCombo(); - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - - if (remainingTime <= 0.0) { - ApplyRecordedCombo(); - ResetComboRecording(); - ImGui::CloseCurrentPopup(); - } - } - } -} diff --git a/src/Features/VR/StereoBlend.cpp b/src/Features/VR/StereoBlend.cpp deleted file mode 100644 index b296d80d1b..0000000000 --- a/src/Features/VR/StereoBlend.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "Features/VR.h" - -#include "Deferred.h" -#include "Features/DynamicCubemaps.h" -#include "Features/ScreenSpaceGI.h" -#include "Features/ScreenSpaceShadows.h" -#include "State.h" -#include "Utils/D3D.h" - -void VR::CompileStereoBlendShaders() -{ - std::vector> defines = { { "VR", "" }, { "FRAMEBUFFER", "" } }; - if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", defines, "cs_5_0"))) - stereoBlendCS.attach(rawPtr); - - auto backCheckDefines = defines; - backCheckDefines.push_back({ "DEBUG_BACKCHECK", "" }); - if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", backCheckDefines, "cs_5_0"))) - stereoBlendDebugBackCheckCS.attach(rawPtr); - - auto blendWeightDefines = defines; - blendWeightDefines.push_back({ "DEBUG_BLEND_WEIGHT", "" }); - if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", blendWeightDefines, "cs_5_0"))) - stereoBlendDebugBlendWeightCS.attach(rawPtr); - - auto edgeDetectionDefines = defines; - edgeDetectionDefines.push_back({ "DEBUG_EDGE_DETECTION", "" }); - if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", edgeDetectionDefines, "cs_5_0"))) - stereoBlendDebugEdgeDetectionCS.attach(rawPtr); - - auto overwriteDefines = defines; - overwriteDefines.push_back({ "STEREO_OVERWRITE", "" }); - if (auto rawPtr = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\VR\\StereoBlendCS.hlsl", overwriteDefines, "cs_5_0"))) - stereoBlendOverwriteCS.attach(rawPtr); -} - -void VR::ClearShaderCache() -{ - stereoBlendCS = nullptr; - stereoBlendDebugBackCheckCS = nullptr; - stereoBlendDebugBlendWeightCS = nullptr; - stereoBlendDebugEdgeDetectionCS = nullptr; - stereoBlendOverwriteCS = nullptr; - stereoOpt.ClearShaderCache(); - - // Framework calls ClearShaderCache without a follow-up SetupResources for these runtime - // CS shaders, so recompile here to leave the feature in a usable state. - CompileStereoBlendShaders(); -} - -bool VR::AnyScreenSpaceEffectLoaded() -{ - return globals::features::screenSpaceGI.loaded || - globals::features::dynamicCubemaps.loaded || - globals::features::screenSpaceShadows.loaded; -} - -void VR::DrawStereoBlend() -{ - bool vrStereoOptActive = IsStereoOptimizationCullingReady(); - - if (!REL::Module::IsVR() || !stereoBlendCopyTex || !stereoBlendCB) - return; - - // Modes 4/5 visualize the overwrite path (mode classification, POM depth) without requiring - // active reprojection — useful when stereoOpt is loaded but reprojection stencil hasn't fired. - bool overwriteVisualizationActive = (settings.StereoBlendDebugMode == 4 || settings.StereoBlendDebugMode == 5) && stereoBlendOverwriteCS; - - if (!vrStereoOptActive && !overwriteVisualizationActive && (!settings.EnableStereoBlend || !stereoBlendCS)) - return; - - if (!vrStereoOptActive && !overwriteVisualizationActive && !AnyScreenSpaceEffectLoaded() && !globals::state->IsDeveloperMode()) - return; - - ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "VR Stereo Blend"); - - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("VR Stereo Blend"); - - auto context = globals::d3d::context; - auto renderer = globals::game::renderer; - - auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - auto* depthSRV = Util::GetCurrentSceneDepthSRV(); - - // Copy main color to read-only texture to avoid read/write race between eyes - context->CopyResource(stereoBlendCopyTex->resource.get(), main.texture); - - auto dispatchCount = Util::GetScreenDispatchCount(true); - float2 resolution = Util::ConvertToDynamic(globals::state->screenSize); - - StereoBlendCB cbData{}; - cbData.FrameDim[0] = resolution.x; - cbData.FrameDim[1] = resolution.y; - cbData.RcpFrameDim[0] = 1.0f / resolution.x; - cbData.RcpFrameDim[1] = 1.0f / resolution.y; - cbData.DepthSigma = settings.StereoBlendDepthSigma; - cbData.MaxBlendFactor = settings.StereoBlendMaxFactor; - cbData.ColorDiffThreshold = settings.StereoBlendColorThreshold; - - bool isOverwriteMode = vrStereoOptActive || overwriteVisualizationActive; - - // Edge tint from reprojection debug visualization flag - if (isOverwriteMode && globals::features::vr.stereoOpt.settings.debugVisualization) - cbData.DebugEdgeTint = 0.3f; - else - cbData.DebugEdgeTint = 0.0f; - - // Debug mode: 0=normal, 1=depth map (mode classification), 2=full blend depth, 3=POM depth heatmap - // StereoBlendDebugMode 4/5 take priority over the individual reprojection debug booleans - if (settings.StereoBlendDebugMode == 4) - cbData.DebugMode = 1u; // "Overwrite": mode texture classification heatmap - else if (settings.StereoBlendDebugMode == 5) - cbData.DebugMode = 3u; // "Overwrite Eye1": POM depth heatmap - else if (isOverwriteMode && globals::features::vr.stereoOpt.settings.debugDepthMap) - cbData.DebugMode = 1u; - else if (isOverwriteMode && globals::features::vr.stereoOpt.settings.debugFullBlendDepth) - cbData.DebugMode = 2u; - else if (isOverwriteMode && globals::features::vr.stereoOpt.settings.debugPOMDepth) - cbData.DebugMode = 3u; - else - cbData.DebugMode = 0u; - - cbData.FullBlendDistance = vrStereoOptActive ? globals::features::vr.stereoOpt.settings.fullBlendDistance : 0.0f; - cbData.POMDepthScale = vrStereoOptActive ? globals::features::vr.stereoOpt.settings.pomDepthScale : 1.0f; - - stereoBlendCB->Update(cbData); - auto cbPtr = stereoBlendCB->CB(); - - auto& motionVectors = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; - - ID3D11ComputeShader* activeCS = stereoBlendCS.get(); - if (isOverwriteMode) { - activeCS = stereoBlendOverwriteCS.get(); - } else if (settings.EnableStereoBlend) { - int effectiveMode = settings.StereoBlendDebugMode; - if (effectiveMode == 1 && stereoBlendDebugBackCheckCS) - activeCS = stereoBlendDebugBackCheckCS.get(); - else if (effectiveMode == 2 && stereoBlendDebugBlendWeightCS) - activeCS = stereoBlendDebugBlendWeightCS.get(); - else if (effectiveMode == 3 && stereoBlendDebugEdgeDetectionCS) - activeCS = stereoBlendDebugEdgeDetectionCS.get(); - } - - // Save and unbind DSV to avoid SRV/DSV conflict on depth buffer in overwrite mode - ID3D11RenderTargetView* savedRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; - ID3D11DepthStencilView* savedDSV = nullptr; - if (isOverwriteMode) { - context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, &savedDSV); - context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, nullptr); - for (auto& rtv : savedRTVs) { - if (rtv) - rtv->Release(); - } - } - - ID3D11ShaderResourceView* srvs[2]{ stereoBlendCopyTex->srv.get(), depthSRV }; - context->CSSetConstantBuffers(1, 1, &cbPtr); - context->CSSetShaderResources(0, 2, srvs); - - if (isOverwriteMode) { - ID3D11ShaderResourceView* modeSRV = globals::features::vr.stereoOpt.GetModeTextureSRV(); - context->CSSetShaderResources(2, 1, &modeSRV); - - // Bind dedicated POM offset SRV (R16_FLOAT, written by Lighting PS at u7) - auto* pomSRV = globals::features::vr.stereoOpt.GetPomOffsetSRV(); - context->CSSetShaderResources(3, 1, &pomSRV); - - ID3D11UnorderedAccessView* uavs[2]{ main.UAV, motionVectors.UAV }; - context->CSSetUnorderedAccessViews(0, 2, uavs, nullptr); - } else { - ID3D11UnorderedAccessView* uavs[1]{ main.UAV }; - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - } - - // Bind linear sampler for hardware bilinear color sampling in overwrite mode - if (isOverwriteMode) { - if (!stereoBlendLinearSampler) { - D3D11_SAMPLER_DESC sampDesc = {}; - sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; - sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; - sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; - globals::d3d::device->CreateSamplerState(&sampDesc, stereoBlendLinearSampler.put()); - Util::SetResourceName(stereoBlendLinearSampler.get(), "VR::StereoBlendLinearSampler"); - } - ID3D11SamplerState* samplers[] = { stereoBlendLinearSampler.get() }; - context->CSSetSamplers(0, 1, samplers); - } - - context->CSSetShader(activeCS, nullptr, 0); - if (isOverwriteMode) { - TracyD3D11Zone(globals::state->tracyCtx, "StereoBlend - Overwrite"); - context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - } else { - TracyD3D11Zone(globals::state->tracyCtx, "StereoBlend - Bilateral"); - context->Dispatch(dispatchCount.x, dispatchCount.y, 1); - } - - // Cleanup - ID3D11ShaderResourceView* nullSRVs[4] = {}; - context->CSSetShaderResources(0, isOverwriteMode ? 4 : 2, nullSRVs); - ID3D11UnorderedAccessView* nullUAVs[2] = {}; - context->CSSetUnorderedAccessViews(0, isOverwriteMode ? 2 : 1, nullUAVs, nullptr); - ID3D11Buffer* nullCB = nullptr; - context->CSSetConstantBuffers(1, 1, &nullCB); - if (isOverwriteMode) { - ID3D11SamplerState* nullSampler[] = { nullptr }; - context->CSSetSamplers(0, 1, nullSampler); - } - context->CSSetShader(nullptr, nullptr, 0); - - // Restore DSV after CS dispatch in overwrite mode - if (isOverwriteMode && savedDSV) { - context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, nullptr); - context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, savedDSV); - for (auto& rtv : savedRTVs) { - if (rtv) - rtv->Release(); - } - savedDSV->Release(); - } - - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); -} diff --git a/src/Features/VR/WandPointing.cpp b/src/Features/VR/WandPointing.cpp deleted file mode 100644 index 2b748bba28..0000000000 --- a/src/Features/VR/WandPointing.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include "Features/VR.h" -#include "RE/B/BSOpenVR.h" -#include "Utils/VRUtils.h" - -#include -#include -#include - -using namespace DirectX::SimpleMath; -using AttachMode = VR::Settings::OverlayAttachMode; - -bool VR::ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) -{ - float controllerM[3][4]; - if (!Util::GetControllerWorldMatrix(controllerIndex, controllerM)) { - return false; - } - Matrix controllerWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(controllerM)); - Vector3 rayOrigin = controllerWorld.Translation(); - Vector3 rayDir = controllerWorld.Forward(); - - // Update debug state - wandState.rayOrigin = rayOrigin; - wandState.rayDirection = rayDir; - Matrix overlayWorld; - if (type == OverlayType::HMD) { - if (settings.VRMenuPositioningMethod == 1) { // Fixed World - overlayWorld = fixedWorldOverlayPosition.m; - } else { // HMD Relative - vr::TrackedDevicePose_t hmdPose; - if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - return false; - if (!hmdPose.bPoseIsValid) - return false; - Matrix hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); - Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); - overlayWorld = offset * hmdWorld; - } - } else { // Controller Relative - vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); - if (attachIndex == vr::k_unTrackedDeviceIndexInvalid) - return false; - - float attachM[3][4]; - if (!Util::GetControllerWorldMatrix(attachIndex, attachM)) - return false; - Matrix attachWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachM)); - - Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); - overlayWorld = offset * attachWorld; - } - - if (settings.VRMenuScale < 1e-4f) - return false; - overlayWorld = Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * overlayWorld; - - Matrix worldToOverlay = overlayWorld.Invert(); - Vector3 localOrigin = Vector3::Transform(rayOrigin, worldToOverlay); - Vector3 localDir = Vector3::TransformNormal(rayDir, worldToOverlay); - - if (std::abs(localDir.z) < 1e-6f) - return false; - - float t = -localOrigin.z / localDir.z; - if (t < 0.0f) - return false; - - Vector3 hit = localOrigin + t * localDir; - - if (hit.x < -0.5f || hit.x > 0.5f || hit.y < -0.5f || hit.y > 0.5f) - return false; - - outUV.x = hit.x + 0.5f; - outUV.y = 0.5f - hit.y; - - return true; -} - -bool VR::ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) -{ - bool intersected = false; - if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { - if (ComputeWandIntersectionForOverlayType(OverlayType::HMD, controllerIndex, outUV)) { - intersected = true; - } - } - if (!intersected && (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both)) { - if (ComputeWandIntersectionForOverlayType(OverlayType::Controller, controllerIndex, outUV)) { - intersected = true; - } - } - - if (intersected) { - wandState.isIntersecting = true; - wandState.uvCoordinates = outUV; - wandState.controllerIndex = controllerIndex; - } else { - wandState.isIntersecting = false; - } - - return intersected; -} - -void VR::UpdateCursorFromWandPointing() -{ - if (!settings.EnableWandPointing || !globals::menu || !globals::menu->IsEnabled) - return; - - ImGuiIO& io = ImGui::GetIO(); - - vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; - - if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { - ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? - ControllerDevice::Secondary : - ControllerDevice::Primary; - pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); - } else { - pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); - } - - if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { - wandState.isIntersecting = false; - return; - } - - ImVec2 uv; - bool intersected = ComputeWandIntersection(pointingController, uv); - - if (intersected) { - float screenX = uv.x * io.DisplaySize.x; - float screenY = uv.y * io.DisplaySize.y; - - screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); - screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); - - io.MousePos = ImVec2(screenX, screenY); - io.AddMousePosEvent(screenX, screenY); - io.MouseDrawCursor = true; - io.WantSetMousePos = true; - } else { - wandState.isIntersecting = false; - io.MouseDrawCursor = false; - io.WantSetMousePos = false; - } -} diff --git a/src/Features/VRStereoOptimizations.cpp b/src/Features/VRStereoOptimizations.cpp deleted file mode 100644 index cf886a2cbf..0000000000 --- a/src/Features/VRStereoOptimizations.cpp +++ /dev/null @@ -1,622 +0,0 @@ -#include "VRStereoOptimizations.h" - -#include "ExtendedMaterials.h" -#include "Globals.h" -#include "I18n/I18n.h" -#define I18N_KEY_PREFIX "feature.vr_stereo." -#include "Menu.h" -#include "State.h" -#include "Utils/D3D.h" -#include "Utils/Game.h" -#include "Utils/UI.h" - -#include - -// JSON enum serialization for StereoMode -NLOHMANN_JSON_SERIALIZE_ENUM(VRStereoOptimizations::StereoMode, { - { VRStereoOptimizations::StereoMode::Off, "Off" }, - { VRStereoOptimizations::StereoMode::Enable, "Enable" }, - }) - -//============================================================================= -// SETTINGS MANAGEMENT -//============================================================================= - -void VRStereoOptimizations::SaveSettings(json& o_json) -{ - o_json["StereoMode"] = settings.stereoMode; - o_json["DisocclusionDepthThreshold"] = settings.disocclusionDepthThreshold; - o_json["FullBlendDistance"] = settings.fullBlendDistance; - o_json["QualityJitterOffset"] = settings.qualityJitterOffset; - o_json["FoveatedRegionRadius"] = settings.foveatedRegionRadius; - o_json["FoveatedRegionCenterX"] = settings.foveatedRegionCenterX; - o_json["FoveatedRegionCenterY"] = settings.foveatedRegionCenterY; - o_json["UseEyeTracking"] = settings.useEyeTracking; - o_json["DebugVisualization"] = settings.debugVisualization; - o_json["DebugSkipMerge"] = settings.debugSkipMerge; - o_json["DebugForceAllStencil"] = settings.debugForceAllStencil; - o_json["DebugForceAllReprojectCS"] = settings.debugForceAllReprojectCS; - o_json["DebugDepthMap"] = settings.debugDepthMap; - o_json["DebugFullBlendDepth"] = settings.debugFullBlendDepth; - o_json["DebugPOMDepth"] = settings.debugPOMDepth; - o_json["POMDepthScale"] = settings.pomDepthScale; - o_json["ForwardOcclusionScale"] = settings.forwardOcclusionScale; -} - -void VRStereoOptimizations::LoadSettings(json& o_json) -{ - auto loadClampedFloat = [&](const char* key, float& dst, float lo, float hi) { - if (auto it = o_json.find(key); it != o_json.end() && it->is_number()) - dst = std::clamp(it->get(), lo, hi); - }; - auto loadBool = [&](const char* key, bool& dst) { - if (auto it = o_json.find(key); it != o_json.end() && it->is_boolean()) - dst = it->get(); - }; - - if (o_json.contains("StereoMode")) - settings.stereoMode = o_json["StereoMode"].get(); - - loadClampedFloat("DisocclusionDepthThreshold", settings.disocclusionDepthThreshold, 0.001f, 0.1f); - loadClampedFloat("QualityJitterOffset", settings.qualityJitterOffset, 0.0f, 1.0f); - loadClampedFloat("FoveatedRegionRadius", settings.foveatedRegionRadius, 0.0f, 1.0f); - loadClampedFloat("FoveatedRegionCenterX", settings.foveatedRegionCenterX, 0.0f, 1.0f); - loadClampedFloat("FoveatedRegionCenterY", settings.foveatedRegionCenterY, 0.0f, 1.0f); - loadClampedFloat("FullBlendDistance", settings.fullBlendDistance, 0.0f, 50000.0f); - loadClampedFloat("POMDepthScale", settings.pomDepthScale, 0.0f, 500.0f); - loadClampedFloat("ForwardOcclusionScale", settings.forwardOcclusionScale, 0.0f, 10.0f); - - loadBool("UseEyeTracking", settings.useEyeTracking); - loadBool("DebugVisualization", settings.debugVisualization); - loadBool("DebugSkipMerge", settings.debugSkipMerge); - loadBool("DebugForceAllStencil", settings.debugForceAllStencil); - loadBool("DebugForceAllReprojectCS", settings.debugForceAllReprojectCS); - loadBool("DebugDepthMap", settings.debugDepthMap); - loadBool("DebugFullBlendDepth", settings.debugFullBlendDepth); - loadBool("DebugPOMDepth", settings.debugPOMDepth); -} - -void VRStereoOptimizations::RestoreDefaultSettings() -{ - settings = {}; -} - -//============================================================================= -// RESOURCE SETUP -//============================================================================= - -void VRStereoOptimizations::SetupResources() -{ - if (!REL::Module::IsVR()) - return; - - auto device = globals::d3d::device; - auto renderer = globals::game::renderer; - - // Constant buffers - paramsCB = eastl::make_unique(ConstantBufferDesc(), "VRStereoOpt::ParamsCB"); - - // Get main RT dimensions for per-eye calculations - auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; - D3D11_TEXTURE2D_DESC mainDesc; - main.texture->GetDesc(&mainDesc); - - // Per-pixel mode texture (R8_UINT, full SBS resolution = both eyes) - { - D3D11_TEXTURE2D_DESC modeDesc{}; - modeDesc.Width = mainDesc.Width; - modeDesc.Height = mainDesc.Height; - modeDesc.MipLevels = 1; - modeDesc.ArraySize = 1; - modeDesc.Format = DXGI_FORMAT_R8_UINT; - modeDesc.SampleDesc.Count = 1; - modeDesc.SampleDesc.Quality = 0; - modeDesc.Usage = D3D11_USAGE_DEFAULT; - modeDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; - modeDesc.CPUAccessFlags = 0; - modeDesc.MiscFlags = 0; - - texPerPixelMode = eastl::make_unique(modeDesc, "VRStereoOpt::PerPixelMode"); - texPerPixelMode->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ - .Format = DXGI_FORMAT_R8_UINT, - .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, - .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); - texPerPixelMode->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ - .Format = DXGI_FORMAT_R8_UINT, - .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, - .Texture2D = { .MipSlice = 0 } }); - } - - // POM offset texture (R16_FLOAT, full SBS resolution) - // Written by Lighting PS (u7) for POM-active pixels, read by StereoBlendCS for depth-aware reprojection. - // Replaces the former overloading of Reflectance.w, so Reflectance stays R11G11B10 with no alpha. - { - D3D11_TEXTURE2D_DESC pomDesc{}; - pomDesc.Width = mainDesc.Width; - pomDesc.Height = mainDesc.Height; - pomDesc.MipLevels = 1; - pomDesc.ArraySize = 1; - pomDesc.Format = DXGI_FORMAT_R16_FLOAT; - pomDesc.SampleDesc.Count = 1; - pomDesc.SampleDesc.Quality = 0; - pomDesc.Usage = D3D11_USAGE_DEFAULT; - pomDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; - pomDesc.CPUAccessFlags = 0; - pomDesc.MiscFlags = 0; - - texPomOffset = eastl::make_unique(pomDesc, "VRStereoOpt::PomOffset"); - texPomOffset->CreateSRV(D3D11_SHADER_RESOURCE_VIEW_DESC{ - .Format = DXGI_FORMAT_R16_FLOAT, - .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, - .Texture2D = { .MostDetailedMip = 0, .MipLevels = 1 } }); - texPomOffset->CreateUAV(D3D11_UNORDERED_ACCESS_VIEW_DESC{ - .Format = DXGI_FORMAT_R16_FLOAT, - .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, - .Texture2D = { .MipSlice = 0 } }); - } - - // Depth-stencil state for stencil write pass: - // Depth test OFF (not rendering geometry), depth writes OFF, stencil ALWAYS + REPLACE with ref=1. - // We use the normal (writable) kMAIN DSV — no simultaneous SRV binding needed. - { - D3D11_DEPTH_STENCIL_DESC dssDesc{}; - dssDesc.DepthEnable = FALSE; - dssDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; - dssDesc.StencilEnable = TRUE; - dssDesc.StencilReadMask = 0xFF; - dssDesc.StencilWriteMask = 0xFF; - dssDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; - dssDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; - dssDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; - dssDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; - dssDesc.BackFace = dssDesc.FrontFace; - - DX::ThrowIfFailed(device->CreateDepthStencilState(&dssDesc, stencilWriteDSS.put())); - Util::SetResourceName(stencilWriteDSS.get(), "VRStereoOpt::StencilWriteDSS"); - } - - // Rasterizer state for stencil write: no culling, no depth clip - { - D3D11_RASTERIZER_DESC rsDesc{}; - rsDesc.FillMode = D3D11_FILL_SOLID; - rsDesc.CullMode = D3D11_CULL_NONE; - rsDesc.DepthClipEnable = FALSE; - - DX::ThrowIfFailed(device->CreateRasterizerState(&rsDesc, stencilWriteRS.put())); - } - - CompileShaders(); - - logger::info("[VRStereoOptimizations] Resources created: mode tex {}x{} (full SBS)", mainDesc.Width, mainDesc.Height); -} - -void VRStereoOptimizations::CompileShaders() -{ - std::vector> csDefines = { - { "VR", nullptr }, - { "FRAMEBUFFER", nullptr } - }; - - std::vector> vspsDefines = { - { "VR", nullptr } - }; - - if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilCS.hlsl", csDefines, "cs_5_0")) - stencilCS.attach(reinterpret_cast(ptr)); - else - logger::error("[VRStereoOptimizations] Failed to compile StencilCS"); - - { - auto debugDefines = csDefines; - debugDefines.push_back({ "DEBUG_DEPTH_MAP", nullptr }); - if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilCS.hlsl", debugDefines, "cs_5_0")) - stencilDebugDepthMapCS.attach(reinterpret_cast(ptr)); - else - logger::error("[VRStereoOptimizations] Failed to compile StencilCS (DEBUG_DEPTH_MAP)"); - } - - if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilWriteVS.hlsl", vspsDefines, "vs_5_0")) - stencilWriteVS.attach(reinterpret_cast(ptr)); - else - logger::error("[VRStereoOptimizations] Failed to compile StencilWriteVS"); - - if (auto* ptr = Util::CompileShader(L"Data\\Shaders\\VRStereoOptimizations\\StencilWritePS.hlsl", vspsDefines, "ps_5_0")) - stencilWritePS.attach(reinterpret_cast(ptr)); - else - logger::error("[VRStereoOptimizations] Failed to compile StencilWritePS"); -} - -void VRStereoOptimizations::ClearShaderCache() -{ - stencilCS = nullptr; - stencilDebugDepthMapCS = nullptr; - stencilWriteVS = nullptr; - stencilWritePS = nullptr; - dssCache.clear(); -} - -void VRStereoOptimizations::Reset() -{ - stencilActive = false; - stencilSwapCount = 0; -} - -void VRStereoOptimizations::ClearPomOffsetTexture() -{ - if (!texPomOffset) - return; - const float clearValue[4] = { kPomOffsetNoData, kPomOffsetNoData, kPomOffsetNoData, kPomOffsetNoData }; - globals::d3d::context->ClearUnorderedAccessViewFloat(texPomOffset->uav.get(), clearValue); -} - -//============================================================================= -// IMGUI SETTINGS -//============================================================================= - -void VRStereoOptimizations::DrawSettings() -{ - const char* modeNames[] = { T("feature.vr_stereo.off", "Off"), T("feature.vr_stereo.enable", "Enable") }; - int currentMode = static_cast(settings.stereoMode); - if (ImGui::Combo(T(TKEY("enable_stereo_reprojection"), "Enable Stereo Reprojection"), ¤tMode, modeNames, IM_ARRAYSIZE(modeNames))) - settings.stereoMode = static_cast(currentMode); - Util::AddTooltip(T(TKEY("enable_stereo_reprojection_tooltip"), "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.")); - - if (globals::game::isVR && settings.stereoMode == StereoMode::Enable && !loaded) { - const auto& themeSettings = Menu::GetSingleton()->GetTheme(); - ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, - "%s", T(TKEY("restart_required"), "Restart is required to enable VR stereo reprojection.")); - } - if (settings.stereoMode == StereoMode::Off) - return; - - ImGui::SliderFloat(T(TKEY("disocclusion_depth_threshold"), "Disocclusion Depth Threshold"), &settings.disocclusionDepthThreshold, 0.001f, 0.1f, "%.4f"); - - ImGui::SliderFloat(T(TKEY("forward_occlusion_scale"), "Forward Occlusion Scale"), &settings.forwardOcclusionScale, 0.0f, 1.0f, "%.2f"); - Util::AddTooltip(T(TKEY("forward_occlusion_scale_tooltip"), "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.")); - - if (globals::state->IsDeveloperMode()) { - if (ImGui::TreeNode(T(TKEY("debug"), "Debug"))) { - ImGui::SliderFloat(T(TKEY("full_blend_distance"), "Full Blend Distance"), &settings.fullBlendDistance, 0.0f, 10000.0f, "%.0f"); - Util::AddTooltip(T(TKEY("full_blend_distance_tooltip"), "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.")); - - ImGui::SliderFloat(T(TKEY("pom_depth_scale"), "POM Depth Scale"), &settings.pomDepthScale, 0.0f, 500.0f, "%.1f"); - Util::AddTooltip(T(TKEY("pom_depth_scale_tooltip"), "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.")); - ImGui::Checkbox(T(TKEY("skip_pixel_reprojection"), "Skip Pixel Reprojection"), &settings.debugSkipMerge); - ImGui::Checkbox(T(TKEY("full_blend_depth_view"), "Full Blend Depth View"), &settings.debugFullBlendDepth); - ImGui::Checkbox(T(TKEY("debug_pom_depth"), "Debug POM Depth"), &settings.debugPOMDepth); - if (settings.debugFullBlendDepth) - ImGui::TextColored(ImVec4(0, 1, 1, 1), "%s", T(TKEY("full_blend_zone_hint"), " Cyan = full blend zone (closer = stronger tint)")); - ImGui::Text("Stencil swaps this frame: %u", stencilSwapCount); - ImGui::TreePop(); - } - } -} - -//============================================================================= -// CONSTANT BUFFER UPDATE -//============================================================================= - -void VRStereoOptimizations::UpdateConstantBuffer() -{ - float2 resolution = Util::ConvertToDynamic(globals::state->screenSize); - - VRStereoOptParams params{}; - params.FrameDim[0] = resolution.x; - params.FrameDim[1] = resolution.y; - params.RcpFrameDim[0] = 1.0f / resolution.x; - params.RcpFrameDim[1] = 1.0f / resolution.y; - params.StereoModeValue = static_cast(settings.stereoMode); - params.DisocclusionThreshold = settings.disocclusionDepthThreshold; - params.EdgeDepthThreshold = settings.edgeDepthThreshold; - params.EdgeWidth = 2; - params.QualityJitter[0] = settings.qualityJitterOffset; - params.QualityJitter[1] = settings.qualityJitterOffset; - params.FoveatedRadius = settings.foveatedRegionRadius; - params.FoveatedCenter[0] = settings.foveatedRegionCenterX; - params.FoveatedCenter[1] = settings.foveatedRegionCenterY; - params.MinEdgeDistance = settings.minEdgeDistance; - params.FullBlendDistance = settings.fullBlendDistance; - params.ForwardOcclusionScale = settings.forwardOcclusionScale; - - paramsCB->Update(params); -} - -//============================================================================= -// PHASE 1: STENCIL CLASSIFICATION + WRITE -//============================================================================= - -void VRStereoOptimizations::DispatchStencil() -{ - if (!REL::Module::IsVR()) - return; - if (settings.stereoMode == StereoMode::Off) - return; - if (!stencilCS || !stencilWriteVS || !stencilWritePS || !texPerPixelMode || !paramsCB || - !stencilWriteDSS || !stencilWriteRS) - return; - - ZoneScoped; - TracyD3D11Zone(globals::state->tracyCtx, "VR Stereo Opt - Stencil"); - - if (globals::state->frameAnnotations) - globals::state->BeginPerfEvent("VR Stereo Opt - Stencil"); - - auto context = globals::d3d::context; - - UpdateConstantBuffer(); - auto cbPtr = paramsCB->CB(); - // Use the same depth source as the rest of the deferred pipeline. - // kMAIN.depthSRV is unpopulated at StartDeferred time (z-prepass has not written to it yet). - // GetCurrentSceneDepthSRV() returns TerrainBlending's blended depth when active, or - // kPOST_ZPREPASS_COPY otherwise — both have valid z-prepass data by this point. - auto* depthSRV = Util::GetCurrentSceneDepthSRV(); - if (!depthSRV) { - logger::warn("[VRStereoOptimizations] DispatchStencil: depthSRV is null, skipping"); - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); - return; - } - - // Dispatch classification CS over Eye 1 region - // Input: t0 = depth, b1 = params CB - // Output: u0 = per-pixel mode texture - { - TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Mode Classify"); - - { - TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Mode Classify Bind"); - - ID3D11ShaderResourceView* srvs[1]{ depthSRV }; - ID3D11UnorderedAccessView* uavs[1]{ texPerPixelMode->uav.get() }; - - context->CSSetConstantBuffers(1, 1, &cbPtr); - context->CSSetShaderResources(0, 1, srvs); - context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); - auto* activeStencilCS = (settings.debugDepthMap && stencilDebugDepthMapCS) ? stencilDebugDepthMapCS.get() : stencilCS.get(); - context->CSSetShader(activeStencilCS, nullptr, 0); - } - - { - TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Mode Classify Dispatch"); - - uint32_t fullWidth = texPerPixelMode->desc.Width; - uint32_t fullHeight = texPerPixelMode->desc.Height; - globals::profiler->BeginPass("VR::StencilClassify"); - context->Dispatch((fullWidth + 7) / 8, (fullHeight + 7) / 8, 1); - globals::profiler->EndPass(); - } - - // Cleanup CS bindings - ID3D11ShaderResourceView* nullSRV = nullptr; - ID3D11UnorderedAccessView* nullUAV = nullptr; - ID3D11Buffer* nullCB = nullptr; - context->CSSetShaderResources(0, 1, &nullSRV); - context->CSSetUnorderedAccessViews(0, 1, &nullUAV, nullptr); - context->CSSetConstantBuffers(1, 1, &nullCB); - context->CSSetShader(nullptr, nullptr, 0); - } - - // Transfer classification to hardware stencil buffer - { - TracyD3D11Zone(globals::state->tracyCtx, "StereoOpt - Stencil Write"); - globals::profiler->BeginPass("VR::StencilWrite"); - ExecuteStencilWritePass(); - globals::profiler->EndPass(); - } - - stencilActive = true; - stencilSwapCount = 0; - - if (globals::state->frameAnnotations) - globals::state->EndPerfEvent(); -} - -void VRStereoOptimizations::ExecuteStencilWritePass() -{ - auto context = globals::d3d::context; - auto renderer = globals::game::renderer; - - // ===== SAVE FULL D3D11 PIPELINE STATE ===== - - ID3D11RenderTargetView* savedRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; - ID3D11DepthStencilView* savedDSV = nullptr; - context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, &savedDSV); - - ID3D11DepthStencilState* savedDSS = nullptr; - UINT savedStencilRef = 0; - context->OMGetDepthStencilState(&savedDSS, &savedStencilRef); - - ID3D11BlendState* savedBlendState = nullptr; - FLOAT savedBlendFactor[4] = {}; - UINT savedSampleMask = 0; - context->OMGetBlendState(&savedBlendState, savedBlendFactor, &savedSampleMask); - - ID3D11RasterizerState* savedRS = nullptr; - context->RSGetState(&savedRS); - - D3D11_VIEWPORT savedViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE] = {}; - UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; - context->RSGetViewports(&numViewports, savedViewports); - - ID3D11VertexShader* savedVS = nullptr; - context->VSGetShader(&savedVS, nullptr, nullptr); - - ID3D11PixelShader* savedPS = nullptr; - context->PSGetShader(&savedPS, nullptr, nullptr); - - ID3D11GeometryShader* savedGS = nullptr; - context->GSGetShader(&savedGS, nullptr, nullptr); - - ID3D11InputLayout* savedInputLayout = nullptr; - context->IAGetInputLayout(&savedInputLayout); - - D3D11_PRIMITIVE_TOPOLOGY savedTopology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED; - context->IAGetPrimitiveTopology(&savedTopology); - - ID3D11ShaderResourceView* savedPSSRV = nullptr; - context->PSGetShaderResources(0, 1, &savedPSSRV); - - ID3D11Buffer* savedPSCB = nullptr; - context->PSGetConstantBuffers(1, 1, &savedPSCB); - - // ===== SET UP STENCIL WRITE PASS ===== - - // Clear stencil buffer to 0 before writing classification. - // The engine's z-prepass may have written stencil values for rendered geometry. - // Without this clear, non-discarded pixels in StencilWritePS could inherit engine stencil - // values that match our NOT_EQUAL ref=1 culling test and incorrectly skip geometry pixels. - // StencilWritePS no longer binds a depth SRV, so we can use the normal writable DSV here. - { - auto& depthData = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - context->ClearDepthStencilView(depthData.views[0], D3D11_CLEAR_STENCIL, 1.0f, 0); - } - - // Use the normal DSV for stencil writes — no depth SRV is bound simultaneously, - // so there is no D3D11 resource hazard and stencil writes are not suppressed. - auto& depthData = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - context->OMSetRenderTargets(0, nullptr, depthData.views[0]); - context->OMSetDepthStencilState(stencilWriteDSS.get(), 1); - context->RSSetState(stencilWriteRS.get()); - - // Eye 1 viewport (right half of SBS buffer) - { - D3D11_TEXTURE2D_DESC mainDesc; - renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].texture->GetDesc(&mainDesc); - - D3D11_VIEWPORT vp{}; - vp.TopLeftX = static_cast(mainDesc.Width / 2); - vp.TopLeftY = 0.0f; - vp.Width = static_cast(mainDesc.Width / 2); - vp.Height = static_cast(mainDesc.Height); - vp.MinDepth = 0.0f; - vp.MaxDepth = 1.0f; - context->RSSetViewports(1, &vp); - } - - // Bind shaders and mode texture - context->VSSetShader(stencilWriteVS.get(), nullptr, 0); - context->PSSetShader(stencilWritePS.get(), nullptr, 0); - context->GSSetShader(nullptr, nullptr, 0); - - ID3D11ShaderResourceView* modeSRV = texPerPixelMode->srv.get(); - context->PSSetShaderResources(0, 1, &modeSRV); - - // Bind params CB to pixel shader (CS and PS have separate CB bindings) - auto cbPtr = paramsCB->CB(); - context->PSSetConstantBuffers(1, 1, &cbPtr); - - // Fullscreen triangle: no VB/IB, procedurally generated in VS - context->IASetInputLayout(nullptr); - context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - context->Draw(3, 0); - - // ===== RESTORE FULL D3D11 PIPELINE STATE ===== - - ID3D11ShaderResourceView* nullSRV = nullptr; - context->PSSetShaderResources(0, 1, &nullSRV); - - context->PSSetConstantBuffers(1, 1, &savedPSCB); - - context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, savedRTVs, savedDSV); - context->OMSetDepthStencilState(savedDSS, savedStencilRef); - context->OMSetBlendState(savedBlendState, savedBlendFactor, savedSampleMask); - context->RSSetState(savedRS); - context->RSSetViewports(numViewports, savedViewports); - context->VSSetShader(savedVS, nullptr, 0); - context->PSSetShader(savedPS, nullptr, 0); - context->GSSetShader(savedGS, nullptr, 0); - context->IASetInputLayout(savedInputLayout); - context->IASetPrimitiveTopology(savedTopology); - context->PSSetShaderResources(0, 1, &savedPSSRV); - - // Release COM references acquired by Get* calls - for (auto& rtv : savedRTVs) { - if (rtv) - rtv->Release(); - } - if (savedDSV) - savedDSV->Release(); - if (savedDSS) - savedDSS->Release(); - if (savedBlendState) - savedBlendState->Release(); - if (savedRS) - savedRS->Release(); - if (savedVS) - savedVS->Release(); - if (savedPS) - savedPS->Release(); - if (savedGS) - savedGS->Release(); - if (savedInputLayout) - savedInputLayout->Release(); - if (savedPSSRV) - savedPSSRV->Release(); - if (savedPSCB) - savedPSCB->Release(); -} - -//============================================================================= -// DSS CACHE: CLONE + STENCIL NOT_EQUAL ENFORCEMENT -//============================================================================= - -ID3D11DepthStencilState* VRStereoOptimizations::GetOrCreateModifiedDSS(ID3D11DepthStencilState* originalDSS) -{ - if (!stencilActive) - return originalDSS; - - // Check cache (nullptr is a valid key — represents D3D11 default state) - if (auto it = dssCache.find(originalDSS); it != dssCache.end()) - return it->second.get(); - - D3D11_DEPTH_STENCIL_DESC desc; - if (originalDSS) { - originalDSS->GetDesc(&desc); - } else { - // D3D11 default state: depth enabled, stencil disabled - desc = {}; - desc.DepthEnable = TRUE; - desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; - desc.DepthFunc = D3D11_COMPARISON_LESS; - desc.StencilEnable = FALSE; - desc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK; - desc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK; - desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; - desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; - desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; - desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; - desc.BackFace = desc.FrontFace; - } - - desc.StencilEnable = TRUE; - desc.StencilReadMask = 0xFF; - desc.StencilWriteMask = 0x00; - - desc.FrontFace.StencilFunc = D3D11_COMPARISON_NOT_EQUAL; - desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; - desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; - desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; - desc.BackFace = desc.FrontFace; - - winrt::com_ptr modifiedDSS; - HRESULT hr = globals::d3d::device->CreateDepthStencilState(&desc, modifiedDSS.put()); - if (FAILED(hr)) { - logger::warn("[VRStereoOptimizations] Failed to create modified DSS (HRESULT: {:#x})", static_cast(hr)); - return originalDSS; - } - - auto* result = modifiedDSS.get(); - dssCache[originalDSS] = std::move(modifiedDSS); - - return result; -} -void VRStereoOptimizations::DeactivateStencil() -{ - if (!stencilActive) - return; - logger::trace("[VRStereoOptimizations] Frame: stencilSwapCount={}", stencilSwapCount); - stencilActive = false; -} - -#undef I18N_KEY_PREFIX diff --git a/src/Features/VRStereoOptimizations.h b/src/Features/VRStereoOptimizations.h deleted file mode 100644 index 4f324395ce..0000000000 --- a/src/Features/VRStereoOptimizations.h +++ /dev/null @@ -1,221 +0,0 @@ -#pragma once - -#include -using json = nlohmann::json; - -#include -#include -#include - -/** - * @brief VR Stereo Rendering Optimizations feature. - * - * Uses hardware stencil culling to skip Eye 1 pixel shading for pixels that can be - * reprojected from Eye 0 via lateral stereo reprojection, then runs a compute shader - * to fill those pixels. This avoids redundant pixel shading in overlapping stereo regions. - * - * Pipeline: - * 1. DispatchStencil() - CS classifies per-pixel reprojection viability into a mode texture, - * then a fullscreen VS/PS pass writes that classification into the stencil buffer. - * 2. (Game renders Eye 1) - Hardware stencil test skips shading for marked pixels. - * 3. VR::DrawStereoBlend() - Stereo overwrite CS reprojects Eye 0 color into skipped Eye 1 pixels. - */ -struct VRStereoOptimizations -{ - bool loaded = false; - - //============================================================================= - // ENUMS - //============================================================================= - - /// Operating mode for stereo reprojection - enum class StereoMode : uint32_t - { - Off = 0, ///< Feature disabled - Enable = 1 ///< Stereo reprojection enabled - }; - - /// Per-pixel classification written by StencilCS - enum PixelMode : uint8_t - { - MODE_DISOCCLUDED = 0, ///< Fully shaded, no reprojection, no blend - MODE_EDGE = 1, ///< Fully shaded + bilateral blend with other eye - MODE_MAIN = 2, ///< Eye 0: no reproject (Perf) / bilateral (Quality). Eye 1: overwrite (Perf) / bilateral (Quality) - MODE_EDGE_NEIGHBOUR = 3, ///< Outer band: background pixels near edge, blended in post-process - MODE_FULL_BLEND = 4, ///< Near-camera pixels: fully shaded in both eyes + bilateral blended - }; - - //============================================================================= - // CONSTANTS - //============================================================================= - - /// Sentinel written to texPomOffset when POM did not run for a pixel. - /// -1.0 = no POM; >= 0.0 = POM ran. Matches Stereo::POM_NO_DATA in Common/VR.hlsli. - static constexpr float kPomOffsetNoData = -1.0f; - - //============================================================================= - // PUBLIC METHODS - //============================================================================= - - void SetupResources(); - void Reset(); - void DrawSettings(); - void SaveSettings(json& o_json); - void LoadSettings(json& o_json); - void RestoreDefaultSettings(); - void ClearShaderCache(); - - //============================================================================= - // SETTINGS - //============================================================================= - - struct Settings - { - StereoMode stereoMode = StereoMode::Off; - float disocclusionDepthThreshold = 0.01f; - float edgeDepthThreshold = 0.05f; - float minEdgeDistance = 5000.0f; ///< Minimum linearized depth for edge AA (game units) - float fullBlendDistance = 0.0f; ///< Linearized depth below which both eyes are fully shaded + blended (game units) - float pomDepthScale = 22.5f; ///< Scale factor for POM depth correction in stereo reprojection - float forwardOcclusionScale = 0.1f; ///< Eye 0 depth multiplier for directional disocclusion; 0 = disabled - bool debugFullBlendDepth = false; ///< Show full blend depth zone as cyan overlay - float qualityJitterOffset = 0.125f; - float foveatedRegionRadius = 0.3f; - float foveatedRegionCenterX = 0.5f; - float foveatedRegionCenterY = 0.5f; - bool useEyeTracking = false; - - // Debug controls - bool debugVisualization = false; - bool debugSkipMerge = false; - bool debugForceAllStencil = false; - bool debugForceAllReprojectCS = false; - bool debugDepthMap = false; - bool debugPOMDepth = false; ///< Show POM depth data (texPomOffset) as heatmap overlay - - } settings; - - //============================================================================= - // GPU CONSTANT BUFFER (must match HLSL cbuffer layout exactly) - //============================================================================= - - struct alignas(16) VRStereoOptParams - { - float FrameDim[2]; // Full stereo buffer dimensions - float RcpFrameDim[2]; // 1.0 / FrameDim - - uint32_t StereoModeValue; // Cast of StereoMode enum (0-3) - float DisocclusionThreshold; - float EdgeDepthThreshold; - uint32_t EdgeWidth; - - float QualityJitter[2]; // Sub-pixel jitter offset (Quality mode) - float FoveatedRadius; - float ForwardOcclusionScale; ///< Eye 0 depth multiplier for directional disocclusion (0 = disabled) - - float FoveatedCenter[2]; // Foveal region center UV - float MinEdgeDistance; - float FullBlendDistance; // Linearized depth for full blend zone - }; - static_assert(sizeof(VRStereoOptParams) % 16 == 0, "VRStereoOptParams must be 16-byte aligned for HLSL cbuffer."); - - //============================================================================= - // PUBLIC API - //============================================================================= - - /** - * @brief Classify Eye 1 pixels and write stencil marks. - * - * Dispatches the stencil classification CS, then performs a fullscreen triangle pass - * to write the classification into the hardware stencil buffer. - * Called from Deferred::StartDeferred() after OverrideBlendStates(). - */ - void DispatchStencil(); - - /** - * @brief Returns true when stencil classification/write resources are ready. - * - * This mirrors DispatchStencil prerequisites except transient per-frame inputs - * like depth SRV availability. - */ - bool CanDispatchStencil() const - { - return loaded && - settings.stereoMode != StereoMode::Off && - !settings.debugSkipMerge && - stencilCS && - stencilWriteVS && - stencilWritePS && - texPerPixelMode && - paramsCB && - stencilWriteDSS && - stencilWriteRS; - } - - /** - * @brief Creates or retrieves a modified DSS with stencil NOT_EQUAL test. - * - * Clones the given DSS with read-only stencil (WriteMask=0x00, Func=NOT_EQUAL, ref=1) - * so that pixels marked by our stencil write pass are skipped during normal rendering. - * Cached per unique input DSS pointer. - * - * @param originalDSS The original depth-stencil state to modify. - * @return Modified DSS with stencil test, or original if creation fails. - */ - ID3D11DepthStencilState* GetOrCreateModifiedDSS(ID3D11DepthStencilState* originalDSS); - - /// Whether the stencil pass is currently active this frame - bool IsStencilActive() const { return stencilActive; } - void NoteStencilSwap() { ++stencilSwapCount; } - - /// Deactivate stencil culling (called from Deferred after geometry rendering completes) - void DeactivateStencil(); - - /// Get mode texture SRV for external consumers (e.g., DeferredCompositeCS Eye 1 skip) - ID3D11ShaderResourceView* GetModeTextureSRV() const { return texPerPixelMode ? texPerPixelMode->srv.get() : nullptr; } - - /// Get POM offset texture SRV for StereoBlendCS (reads per-pixel parallax depth offset) - ID3D11ShaderResourceView* GetPomOffsetSRV() const { return texPomOffset ? texPomOffset->srv.get() : nullptr; } - - /// Get POM offset texture UAV for PS writes during deferred lighting (injected at u7) - ID3D11UnorderedAccessView* GetPomOffsetUAV() const { return texPomOffset ? texPomOffset->uav.get() : nullptr; } - - /// Clear the POM offset texture to -1.0 (no-POM sentinel) at the start of each deferred frame - void ClearPomOffsetTexture(); - -private: - //============================================================================= - // INTERNAL METHODS - //============================================================================= - - /// Fullscreen triangle pass: reads mode texture, writes stencil ref=1 for MODE_MAIN pixels - void ExecuteStencilWritePass(); - - /// Compiles all shaders used by this feature - void CompileShaders(); - - /// Updates the constant buffer with current settings and frame dimensions - void UpdateConstantBuffer(); - - //============================================================================= - // GPU RESOURCES - //============================================================================= - - eastl::unique_ptr paramsCB; - eastl::unique_ptr texPerPixelMode; ///< R8_UINT classification texture (full SBS resolution) - eastl::unique_ptr texPomOffset; ///< R16_FLOAT POM depth offset written by Lighting PS, read by StereoBlendCS - - winrt::com_ptr stencilWriteDSS; - winrt::com_ptr stencilWriteRS; - - winrt::com_ptr stencilCS; - winrt::com_ptr stencilDebugDepthMapCS; - winrt::com_ptr stencilWriteVS; - winrt::com_ptr stencilWritePS; - - /// Cache of original DSS -> modified DSS with stencil NOT_EQUAL enforcement - std::unordered_map> dssCache; - - bool stencilActive = false; - uint32_t stencilSwapCount = 0; -}; diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index db9f646b10..60a1148d17 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -146,39 +146,14 @@ void VolumetricLighting::SaveSettings(json& o_json) void VolumetricLighting::RestoreDefaultSettings() { settings = {}; - if (globals::game::isVR) - Util::ResetGameSettingsToDefaults(hiddenVRSettings); } void VolumetricLighting::DataLoaded() { - auto shaderCache = globals::shaderCache; - const static auto address = REL::Offset{ 0x1ec6b88 }.address(); - bool& bDepthBufferCulling = *reinterpret_cast(address); - - if (REL::Module::IsVR() && bDepthBufferCulling && shaderCache->IsDiskCache()) { - // clear cache to fix bug caused by bDepthBufferCulling - logger::info("Force clearing cache due to bDepthBufferCulling"); - shaderCache->Clear(); - } } void VolumetricLighting::PostPostLoad() { - if (REL::Module::IsVR()) { - if (settings.ExteriorEnabled || settings.InteriorEnabled) - EnableBooleanSettings(hiddenVRSettings, GetName()); - auto address = REL::RelocationID(100475, 0).address() + 0x45b; // AE not needed, VR only hook - logger::info("[{}] Hooking CopyResource at {:x}", GetName(), address); - REL::safe_fill(address, REL::NOP, 7); - stl::write_thunk_call(address); - - // Skip volumetric lighting rendering - REL::safe_write(REL::RelocationID(35560, 0).address() + REL::Relocate(0x254, 0), &REL::JMP8, 1); - // Move it to render after depth to ensure camera matches rest of scene - stl::write_thunk_call(REL::RelocationID(35560, 0).address() + REL::Relocate(0x2EE, 0)); - } - bEnableVolumetricLighting = reinterpret_cast(REL::RelocationID(527940, 414913).address()); gVolumetricLightingSizeLow = reinterpret_cast(REL::RelocationID(527970, 414916).address()); gVolumetricLightingSizeMedium = reinterpret_cast(REL::RelocationID(527973, 414919).address()); @@ -200,10 +175,8 @@ void VolumetricLighting::SetupResources() void VolumetricLighting::EarlyPrepass() { - auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); - - int32_t width = static_cast(renderSize.x); - int32_t height = static_cast(renderSize.y); + int32_t width = static_cast((float)globals::game::graphicsState->screenWidth); + int32_t height = static_cast((float)globals::game::graphicsState->screenHeight); if (width != vlData.screenX || height != vlData.screenY) { blurHCS = nullptr; @@ -231,17 +204,11 @@ void VolumetricLighting::EarlyPrepass() void VolumetricLighting::SetupVL() { if (inInterior) { - if (globals::game::isVR) - SetBooleanSettings(hiddenVRSettings, GetName(), settings.InteriorEnabled && inInteriorWithSun); - else - *bEnableVolumetricLighting = settings.InteriorEnabled && inInteriorWithSun; + *bEnableVolumetricLighting = settings.InteriorEnabled && inInteriorWithSun; *gVolumetricLightingSizeHigh = static_cast(settings.InteriorQuality) == Quality::Custom ? settings.InteriorCustomSize : defaultSizeHigh; SetVLQuality(GetVLDescriptor(), settings.InteriorQuality); } else { - if (globals::game::isVR) - SetBooleanSettings(hiddenVRSettings, GetName(), settings.ExteriorEnabled); - else - *bEnableVolumetricLighting = settings.ExteriorEnabled; + *bEnableVolumetricLighting = settings.ExteriorEnabled; *gVolumetricLightingSizeHigh = static_cast(settings.ExteriorQuality) == Quality::Custom ? settings.ExteriorCustomSize : defaultSizeHigh; SetVLQuality(GetVLDescriptor(), settings.ExteriorQuality); } @@ -261,20 +228,6 @@ void VolumetricLighting::SetVLQuality(VolumetricLightingDescriptor& descriptor, func(descriptor, std::clamp(quality, 0, 2)); } -void VolumetricLighting::RenderVolumetricLighting(VolumetricLightingDescriptor* descriptor, RE::NiCamera* camera, bool flag) -{ - using func_t = decltype(&VolumetricLighting::RenderVolumetricLighting); - static REL::Relocation func{ REL::RelocationID(100306, 0) }; - func(descriptor, camera, flag); -} - -void VolumetricLighting::RenderDepth::thunk() -{ - func(); - if (globals::features::volumetricLighting.bEnableVolumetricLighting) - RenderVolumetricLighting(&GetVLDescriptor(), RE::Main::WorldRootCamera(), false); -} - RE::BSImagespaceShader* VolumetricLighting::CreateShader(const std::string_view& name, const std::string_view& fileName, RE::BSComputeShader* computeShader) { auto shader = RE::BSImagespaceShader::Create(); @@ -331,21 +284,4 @@ void VolumetricLighting::SetGroupCountsVCS(uint32_t& threadGroupCountY) const threadGroupCountY = (vlData.screenY + BlurThreadGroupSizeY - BlurWindow * 2u - 1u) / (BlurThreadGroupSizeY - BlurWindow * 2u); } -void VolumetricLighting::CopyResource::thunk(ID3D11DeviceContext* a_this, ID3D11Resource* a_renderTarget, ID3D11Resource* a_renderTargetSource) -{ - // In VR with dynamic resolution enabled, there's a bug with the depth stencil. - // The depth stencil passed to IsFullScreenVR is scaled down incorrectly. - // The fix is to stop a CopyResource from replacing kMAIN_COPY with kMAIN after - // ISApplyVolumetricLighting because it clobbers a properly scaled kMAIN_COPY. - // The kMAIN_COPY does not appear to be used in the remaining frame after - // ISApplyVolumetricLighting except for IsFullScreenVR. - // But, the copy might have to be done manually later after IsFullScreenVR if - // used in the next frame. - - auto& singleton = globals::features::volumetricLighting; - if (!(Util::IsDynamicResolution() && singleton.bEnableVolumetricLighting)) { - a_this->CopyResource(a_renderTarget, a_renderTargetSource); - } -} - #undef I18N_KEY_PREFIX diff --git a/src/Features/VolumetricLighting.h b/src/Features/VolumetricLighting.h index a6bcf21c95..2b27656bbd 100644 --- a/src/Features/VolumetricLighting.h +++ b/src/Features/VolumetricLighting.h @@ -22,8 +22,6 @@ struct VolumetricLighting : Feature Settings settings; - bool enabledAtBoot = false; - virtual inline std::string GetName() override { return "Volumetric Lighting"; } virtual std::string GetDisplayName() override { return T("feature.volumetric_lighting.name", "Volumetric Lighting"); } virtual inline std::string GetShortName() override { return "VolumetricLighting"; } @@ -48,24 +46,6 @@ struct VolumetricLighting : Feature virtual void SetupResources() override; virtual void EarlyPrepass() override; - std::map hiddenVRSettings{ - { "bEnableVolumetricLighting:Display", { "Enable VL Shaders (INI) ", - "Enables volumetric lighting effects by creating shaders. " - "Needed at startup. ", - 0x1ed63d8, true, false, true } }, - { "bVolumetricLightingEnable:Display", { "Enable VL (INI))", "Enables volumetric lighting. ", 0x3485360, true, false, true } }, - { "bVolumetricLightingUpdateWeather:Display", { "Enable Volumetric Lighting (Weather) (INI) ", - "Enables volumetric lighting for weather. " - "Only used during startup and used to set bVLWeatherUpdate.", - 0x3485361, true, false, true } }, - { "bVLWeatherUpdate", { "Enable VL (Weather)", "Enables volumetric lighting for weather.", 0x3485363, true, false, true } }, - { "bVolumetricLightingEnabled_143232EF0", { "Enable VL (Papyrus) ", - "Enables volumetric lighting. " - "This is the Papyrus command. ", - REL::Relocate(0x3232ef0, 0, 0x3485362), true, false, true } }, - }; - - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; static RE::BSImagespaceShader* CreateShader(const std::string_view& name, const std::string_view& fileName, RE::BSComputeShader* computeShader); @@ -77,20 +57,6 @@ struct VolumetricLighting : Feature void SetGroupCountsHCS(uint32_t& threadGroupCountX) const; void SetGroupCountsVCS(uint32_t& threadGroupCountY) const; - // hooks - - struct CopyResource - { - static void thunk(ID3D11DeviceContext* a_this, ID3D11Resource* a_renderTarget, ID3D11Resource* a_renderTargetSource); - static inline REL::Relocation func; - }; - - struct RenderDepth - { - static void thunk(); - static inline REL::Relocation func; - }; - private: struct VolumetricLightingDescriptor {}; @@ -98,8 +64,6 @@ struct VolumetricLighting : Feature static const char* FromUnits(int32_t value, int32_t unitScale); static VolumetricLightingDescriptor& GetVLDescriptor(); static void SetVLQuality(VolumetricLightingDescriptor& descriptor, std::uint32_t quality); - static void RenderVolumetricLighting(VolumetricLightingDescriptor* descriptor, RE::NiCamera* camera, bool flag); - void DrawVolumetricLightingSettings(int32_t& quality, TextureSize& customSize, bool isInterior, bool inLocationType); TextureSize& FetchCurrentSizeInUnits(bool interior); void SetupVL(); diff --git a/src/Features/VolumetricShadows.cpp b/src/Features/VolumetricShadows.cpp index 34459618ed..b59bf36c94 100644 --- a/src/Features/VolumetricShadows.cpp +++ b/src/Features/VolumetricShadows.cpp @@ -372,7 +372,7 @@ struct CreateDepthStencil_VolumetricLighting void VolumetricShadows::PostPostLoad() { - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x9DC, 0x9DC, 0xC60)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x9DC, 0x9DC)); } bool VolumetricShadows::HasShaderDefine(RE::BSShader::Type) diff --git a/src/Features/VolumetricShadows.h b/src/Features/VolumetricShadows.h index e7d5a385eb..0830395070 100644 --- a/src/Features/VolumetricShadows.h +++ b/src/Features/VolumetricShadows.h @@ -64,7 +64,6 @@ struct VolumetricShadows : Feature virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/WaterEffects.h b/src/Features/WaterEffects.h index 87a79d1f36..e64a04ea30 100644 --- a/src/Features/WaterEffects.h +++ b/src/Features/WaterEffects.h @@ -28,6 +28,5 @@ struct WaterEffects : Feature virtual void Prepass() override; - virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/WetnessEffects.h b/src/Features/WetnessEffects.h index ff4c28d4d2..f7b31e4a96 100644 --- a/src/Features/WetnessEffects.h +++ b/src/Features/WetnessEffects.h @@ -118,7 +118,6 @@ struct WetnessEffects : Feature virtual void RestoreDefaultSettings() override; - virtual bool SupportsVR() override { return true; }; // Override to provide weather analysis configuration virtual WeatherAnalysisConfig GetWeatherAnalysisConfig() const override diff --git a/src/FrameAnnotations.cpp b/src/FrameAnnotations.cpp index 14fafd6e98..52c51ae5da 100644 --- a/src/FrameAnnotations.cpp +++ b/src/FrameAnnotations.cpp @@ -16,8 +16,7 @@ namespace FrameAnnotations if (globals::state && globals::state->IsDeveloperMode()) { uint16_t packed = static_cast(EffectType); uint16_t se = RE::ImageSpaceManager::GetSEIndex(EffectType); - uint16_t vr = RE::ImageSpaceManager::GetVRIndex(EffectType); - std::string packedString = std::format(" (packed: 0x{:X}, SE: {}, VR: {})", packed, se, vr); + std::string packedString = std::format(" (packed: 0x{:X}, SE: {})", packed, se); return enumName + packedString; } else { return enumName; @@ -220,39 +219,6 @@ namespace FrameAnnotations static inline REL::Relocation func; }; - struct VR_RenderDepth_DownscaleDepthBuffer - { - static void thunk(RE::BSSceneGraph* a1) - { - globals::state->BeginPerfEvent("DownscaleDepthBuffer"); - func(a1); - globals::state->EndPerfEvent(); - }; - static inline REL::Relocation func; - }; - - struct VR_RenderDepth_BSOBBOcclusionTestingShader - { - static void thunk(RE::BSImagespaceShader* a_this, RE::ImageSpaceEffectParam* a_param) - { - globals::state->BeginPerfEvent("BSOBBOcclusionTestingShader"); - func(a_this, a_param); - globals::state->EndPerfEvent(); - }; - static inline REL::Relocation func; - }; - - struct VR_UpscaleDepthBuffer - { - static void thunk(RE::ImageSpaceManager* a_this, unsigned int a2, RE::RENDER_TARGET a_target, RE::RENDER_TARGET a_target2, __int64 a5, bool a6) - { - globals::state->BeginPerfEvent("UpscaleDepthBuffer"); - func(a_this, a2, a_target, a_target2, a5, a6); - globals::state->EndPerfEvent(); - }; - static inline REL::Relocation func; - }; - struct Main_RenderWorld { static void thunk(bool a1) @@ -958,74 +924,6 @@ namespace FrameAnnotations RE::VTABLE_BSImagespaceShaderISUnderwaterMask[0]); stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( RE::VTABLE_BSImagespaceShaderWaterFlow[0]); - // VR-only shaders - if (globals::game::isVR) { - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderCopyDepthBuffer[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderCopyDepthBuffer_DR[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderDepthBuffer4xDownscale[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderISFullScreenVR[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderTransformLvl7PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl6PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl5PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl4PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl3PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl2PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl1PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderLvl0PreTest[3]); - stl::write_vfunc<0x1, BSImagespaceShader_Render>( - RE::VTABLE_BSImagespaceShaderSetupPreTest[3]); - - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderCopyDepthBuffer[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderCopyDepthBuffer_DR[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderDepthBuffer4xDownscale[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderISFullScreenVR[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderTransformLvl7PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl6PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl5PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl4PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl3PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl2PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl1PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderLvl0PreTest[0]); - stl::write_vfunc<0xC, BSImagespaceShader_Dispatch>( - RE::VTABLE_BSImagespaceShaderSetupPreTest[0]); - stl::write_thunk_call(REL::RelocationID(100421, 107139).address() + REL::Relocate(0x37f, 0)); - stl::write_thunk_call(REL::RelocationID(100421, 107139).address() + REL::Relocate(0x3b1, 0)); - stl::write_thunk_call(REL::Offset(0x13246AE).address()); - } - stl::write_vfunc<0x2A, BSShaderAccumulator_FinishAccumulatingDispatch>( RE::VTABLE_BSShaderAccumulator[0]); @@ -1057,7 +955,7 @@ namespace FrameAnnotations auto renderer = globals::game::renderer; for (size_t renderTargetIndex = 0; - renderTargetIndex < Util::GetRenderTargetCount(); ++renderTargetIndex) { + renderTargetIndex < RE::RENDER_TARGETS::kTOTAL; ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast(renderTargetIndex)); if (auto texture = renderer->GetRuntimeData().renderTargets[renderTargetIndex].texture) { @@ -1078,7 +976,7 @@ namespace FrameAnnotations } for (size_t renderTargetIndex = 0; - renderTargetIndex < Util::GetDepthStencilCount(); + renderTargetIndex < RE::RENDER_TARGETS_DEPTHSTENCIL::kTOTAL; ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast( diff --git a/src/Globals.cpp b/src/Globals.cpp index 15cba11777..d2944bf2aa 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -31,7 +31,6 @@ #include "Features/TerrainVariation.h" #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" -#include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -78,7 +77,6 @@ namespace globals TerrainShadows terrainShadows{}; UnifiedWater unifiedWater{}; VolumetricLighting volumetricLighting{}; - VR vr{}; WaterEffects waterEffects{}; PerformanceOverlay performanceOverlay{}; WetnessEffects wetnessEffects{}; @@ -104,7 +102,6 @@ namespace globals RE::BSGraphics::Renderer* renderer = nullptr; RE::BSShaderManager::State* smState = nullptr; RE::TES* tes = nullptr; - bool isVR = false; RE::MemoryManager* memoryManager = nullptr; RE::INISettingCollection* iniSettingCollection = nullptr; RE::INIPrefSettingCollection* iniPrefSettingCollection = nullptr; @@ -177,7 +174,6 @@ namespace globals graphicsState = RE::BSGraphics::State::GetSingleton(); renderer = RE::BSGraphics::Renderer::GetSingleton(); smState = &RE::BSShaderManager::State::GetSingleton(); - isVR = REL::Module::IsVR(); iniSettingCollection = RE::INISettingCollection::GetSingleton(); iniPrefSettingCollection = RE::INIPrefSettingCollection::GetSingleton(); gameSettingCollection = RE::GameSettingCollection::GetSingleton(); @@ -186,9 +182,9 @@ namespace globals cameraFar = (float*)(REL::RelocationID(517032, 403540).address() + 0x44); deltaTime = (float*)REL::RelocationID(523660, 410199).address(); - currentPixelShader = GET_INSTANCE_MEMBER_PTR(currentPixelShader, shadowState); - currentVertexShader = GET_INSTANCE_MEMBER_PTR(currentVertexShader, shadowState); - stateUpdateFlags = GET_INSTANCE_MEMBER_PTR(stateUpdateFlags, shadowState); + currentPixelShader = &(shadowState->GetRuntimeData().currentPixelShader); + currentVertexShader = &(shadowState->GetRuntimeData().currentVertexShader); + stateUpdateFlags = &(shadowState->GetRuntimeData().stateUpdateFlags); ui = RE::UI::GetSingleton(); calendar = RE::Calendar::GetSingleton(); @@ -243,13 +239,8 @@ namespace globals void CacheFramebuffer() { using namespace game; - if (REL::Module::IsVR()) { - auto frameBufferVR = (FrameBufferVR*)mappedFrameBuffer->pData; - frameBufferCached.vr = *frameBufferVR; - } else { - auto frameBuffer = (FrameBuffer*)mappedFrameBuffer->pData; - frameBufferCached.nonVR = *frameBuffer; - } + auto frameBuffer = (FrameBuffer*)mappedFrameBuffer->pData; + frameBufferCached.data = *frameBuffer; mappedFrameBuffer = nullptr; } @@ -291,118 +282,9 @@ namespace globals static inline REL::Relocation func; }; - /** - * @brief Hooked OMSetRenderTargets — injects POM offset UAV at slot 7 when in the deferred pass. - * - * vtable index 33 for ID3D11DeviceContext::OMSetRenderTargets. - * After Skyrim binds the deferred MRT (clearing all UAVs), this hook re-adds the POM offset - * UAV at slot u7 so the Lighting PS (VR_STEREO_OPT permutation) can write per-pixel parallax - * depth offsets without overloading Reflectance.w. - */ - struct ID3D11DeviceContext_OMSetRenderTargets - { - static void STDMETHODCALLTYPE thunk(ID3D11DeviceContext* This, UINT NumViews, ID3D11RenderTargetView* const* ppRenderTargetViews, ID3D11DepthStencilView* pDepthStencilView) - { - func(This, NumViews, ppRenderTargetViews, pDepthStencilView); - - // D3D11 handles any SRV/UAV conflict automatically (silently unbinds the UAV when - // the same resource is later bound as an SRV), so no NumViews guard is needed. - if (globals::deferred->deferredPass) { - auto& stereoOpt = globals::features::vr.stereoOpt; - if (stereoOpt.loaded) { - if (auto* uav = stereoOpt.GetPomOffsetUAV()) { - This->OMSetRenderTargetsAndUnorderedAccessViews( - D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL, nullptr, nullptr, - 7, 1, &uav, nullptr); - } - } - } - } - static inline REL::Relocation func; - }; - - /** - * @brief Hooked OMSetDepthStencilState — replaces DSS with stencil-enforcing version when VR stereo opt is active. - * - * vtable index 36 for ID3D11DeviceContext::OMSetDepthStencilState. - * When VRStereoOptimizations has written stencil marks, this hook transparently swaps - * the game's DSS for a modified version that adds a stencil NOT_EQUAL test, causing - * marked Eye 1 pixels to be skipped during normal rendering. - */ - struct ID3D11DeviceContext_OMSetDepthStencilState - { - static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilState* pDepthStencilState, UINT StencilRef) - { - if (globals::game::isVR) { - auto& stereoOpt = globals::features::vr.stereoOpt; - if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { - pDepthStencilState = stereoOpt.GetOrCreateModifiedDSS(pDepthStencilState); - stereoOpt.NoteStencilSwap(); - StencilRef = 1; // Must match the ref written by our stencil pass - } - } - func(This, pDepthStencilState, StencilRef); - } - static inline REL::Relocation func; - }; - - /** - * @brief Hooked ClearDepthStencilView — blocks stencil clears when VR stereo opt stencil is active. - * - * vtable index 53 for ID3D11DeviceContext::ClearDepthStencilView. - * Prevents the game from clearing our stencil marks between the stencil write and - * the stereo overwrite blend pass by stripping the D3D11_CLEAR_STENCIL flag. - */ - struct ID3D11DeviceContext_ClearDepthStencilView - { - static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilView* pDepthStencilView, UINT ClearFlags, FLOAT Depth, UINT8 Stencil) - { - if (globals::game::isVR) { - auto& stereoOpt = globals::features::vr.stereoOpt; - if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { - // Only protect the main scene DSV — allow other DSVs to clear normally - auto renderer = globals::game::renderer; - auto& mainDepth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - if (mainDepth.views[0]) { - // Compare the DSV being cleared against the main scene DSV - ID3D11Resource* clearRes = nullptr; - ID3D11Resource* mainRes = nullptr; - pDepthStencilView->GetResource(&clearRes); - mainDepth.views[0]->GetResource(&mainRes); - bool isMainDSV = (clearRes == mainRes); - if (clearRes) - clearRes->Release(); - if (mainRes) - mainRes->Release(); - if (isMainDSV) { - ClearFlags &= ~D3D11_CLEAR_STENCIL; - if (ClearFlags == 0) - return; - } - } - } - } - func(This, pDepthStencilView, ClearFlags, Depth, Stencil); - } - static inline REL::Relocation func; - }; - - /** - * @brief Installs hooks on the Map and Unmap methods of the provided D3D11 device context. - * - * This enables interception of resource mapping and unmapping operations for frame buffer caching. - */ void InstallD3DHooks(ID3D11DeviceContext* a_context) { stl::detour_vfunc<14, ID3D11DeviceContext_Map>(a_context); stl::detour_vfunc<15, ID3D11DeviceContext_Unmap>(a_context); - - // VR stereo optimization hooks: installed only when stereo reprojection is enabled at startup. - // Changing stereoMode at runtime requires a restart; the UI communicates this to the user. - if (globals::game::isVR && globals::features::vr.stereoOpt.settings.stereoMode != VRStereoOptimizations::StereoMode::Off) { - stl::detour_vfunc<33, ID3D11DeviceContext_OMSetRenderTargets>(a_context); - stl::detour_vfunc<36, ID3D11DeviceContext_OMSetDepthStencilState>(a_context); - stl::detour_vfunc<53, ID3D11DeviceContext_ClearDepthStencilView>(a_context); - } } } diff --git a/src/Globals.h b/src/Globals.h index 9fd5bb081c..e1780a14e0 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -26,7 +26,6 @@ struct TerrainHelper; struct TerrainShadows; struct UnifiedWater; struct VolumetricLighting; -struct VR; struct WaterEffects; struct PerformanceOverlay; struct WetnessEffects; @@ -86,7 +85,6 @@ namespace globals extern TerrainShadows terrainShadows; extern UnifiedWater unifiedWater; extern VolumetricLighting volumetricLighting; - extern VR vr; extern WaterEffects waterEffects; extern PerformanceOverlay performanceOverlay; extern WetnessEffects wetnessEffects; @@ -100,9 +98,6 @@ namespace globals extern TruePBR truePBR; extern Skin skin; - namespace llf - { - } } struct FrameBuffer @@ -124,92 +119,25 @@ namespace globals float4 DynamicResolutionParams2; }; - struct FrameBufferVR + struct FrameBufferCache { - // Must match HLSL VR layout exactly - packoffsets c0 to c86 - Matrix CameraView[2]; // packoffset(c0) - 8 registers - Matrix CameraProj[2]; // packoffset(c8) - 8 registers - Matrix CameraViewProj[2]; // packoffset(c16) - 8 registers - Matrix CameraViewProjUnjittered[2]; // packoffset(c24) - 8 registers - Matrix CameraPreviousViewProjUnjittered[2]; // packoffset(c32) - 8 registers - Matrix CameraProjUnjittered[2]; // packoffset(c40) - 8 registers - Matrix CameraProjUnjitteredInverse[2]; // packoffset(c48) - 8 registers - Matrix CameraViewInverse[2]; // packoffset(c56) - 8 registers - Matrix CameraViewProjInverse[2]; // packoffset(c64) - 8 registers - Matrix CameraProjInverse[2]; // packoffset(c72) - 8 registers - float4 CameraPosAdjust[2]; // packoffset(c80) - 2 registers - float4 CameraPreviousPosAdjust[2]; // packoffset(c82) - 2 registers - float4 FrameParams; // packoffset(c84) - 1 register - float4 DynamicResolutionParams1; // packoffset(c85) - 1 register - float4 DynamicResolutionParams2; // packoffset(c86) - 1 register - }; - - union FrameBufferCache - { - FrameBuffer nonVR; - FrameBufferVR vr; - - // Helper functions for VR-agnostic access to eye 0 (or single eye) - const Matrix& GetCameraView(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraView[eyeIndex] : nonVR.CameraView; - } - const Matrix& GetCameraProj(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraProj[eyeIndex] : nonVR.CameraProj; - } - const Matrix& GetCameraViewProj(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraViewProj[eyeIndex] : nonVR.CameraViewProj; - } - const Matrix& GetCameraViewProjUnjittered(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraViewProjUnjittered[eyeIndex] : nonVR.CameraViewProjUnjittered; - } - const Matrix& GetCameraPreviousViewProjUnjittered(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraPreviousViewProjUnjittered[eyeIndex] : nonVR.CameraPreviousViewProjUnjittered; - } - const Matrix& GetCameraProjUnjittered(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraProjUnjittered[eyeIndex] : nonVR.CameraProjUnjittered; - } - const Matrix& GetCameraProjUnjitteredInverse(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraProjUnjitteredInverse[eyeIndex] : nonVR.CameraProjUnjitteredInverse; - } - const Matrix& GetCameraViewInverse(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraViewInverse[eyeIndex] : nonVR.CameraViewInverse; - } - const Matrix& GetCameraViewProjInverse(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraViewProjInverse[eyeIndex] : nonVR.CameraViewProjInverse; - } - const Matrix& GetCameraProjInverse(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraProjInverse[eyeIndex] : nonVR.CameraProjInverse; - } - const float4& GetCameraPosAdjust(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraPosAdjust[eyeIndex] : nonVR.CameraPosAdjust; - } - const float4& GetCameraPreviousPosAdjust(uint32_t eyeIndex = 0) const - { - return REL::Module::IsVR() ? vr.CameraPreviousPosAdjust[eyeIndex] : nonVR.CameraPreviousPosAdjust; - } - const float4& GetFrameParams() const - { - return REL::Module::IsVR() ? vr.FrameParams : nonVR.FrameParams; - } - const float4& GetDynamicResolutionParams1() const - { - return REL::Module::IsVR() ? vr.DynamicResolutionParams1 : nonVR.DynamicResolutionParams1; - } - const float4& GetDynamicResolutionParams2() const - { - return REL::Module::IsVR() ? vr.DynamicResolutionParams2 : nonVR.DynamicResolutionParams2; - } + FrameBuffer data; + + const Matrix& GetCameraView() const { return data.CameraView; } + const Matrix& GetCameraProj() const { return data.CameraProj; } + const Matrix& GetCameraViewProj() const { return data.CameraViewProj; } + const Matrix& GetCameraViewProjUnjittered() const { return data.CameraViewProjUnjittered; } + const Matrix& GetCameraPreviousViewProjUnjittered() const { return data.CameraPreviousViewProjUnjittered; } + const Matrix& GetCameraProjUnjittered() const { return data.CameraProjUnjittered; } + const Matrix& GetCameraProjUnjitteredInverse() const { return data.CameraProjUnjitteredInverse; } + const Matrix& GetCameraViewInverse() const { return data.CameraViewInverse; } + const Matrix& GetCameraViewProjInverse() const { return data.CameraViewProjInverse; } + const Matrix& GetCameraProjInverse() const { return data.CameraProjInverse; } + const float4& GetCameraPosAdjust() const { return data.CameraPosAdjust; } + const float4& GetCameraPreviousPosAdjust() const { return data.CameraPreviousPosAdjust; } + const float4& GetFrameParams() const { return data.FrameParams; } + const float4& GetDynamicResolutionParams1() const { return data.DynamicResolutionParams1; } + const float4& GetDynamicResolutionParams2() const { return data.DynamicResolutionParams2; } }; namespace game @@ -219,7 +147,6 @@ namespace globals extern RE::BSGraphics::Renderer* renderer; extern RE::BSShaderManager::State* smState; extern RE::TES* tes; - extern bool isVR; extern RE::MemoryManager* memoryManager; extern RE::INISettingCollection* iniSettingCollection; extern RE::INIPrefSettingCollection* iniPrefSettingCollection; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index b11a89eca7..9abd07fc9c 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -17,7 +17,6 @@ #include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Upscaling.h" -#include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "ShaderTools/BSShaderHooks.h" @@ -239,7 +238,7 @@ namespace WaterBlendHistory { static void thunk(void* imageSpaceShader, RE::BSTriShape* shape, RE::ImageSpaceEffectParam* param) { - GET_INSTANCE_MEMBER(renderTargets, globals::game::shadowState) + auto& renderTargets = globals::game::shadowState->GetRuntimeData().renderTargets; // Clear stale coverage left by discarded non-water pixels const float clearColor[4] = { 0.f, 0.f, 0.f, 0.f }; @@ -391,7 +390,7 @@ struct BSShaderRenderTargets_Create */ static inline Util::GameSetting iNumFocusShadow{ "Number of Focus Shadows (INI)", "Controls the number of focus shadows.", - REL::Relocate(0, 0, 0x1ed6368), 4, 0, 4 }; + static_cast(0), 4, 0, 4 }; static void thunk() { @@ -420,33 +419,8 @@ struct BSInputDeviceManager_PollInputDevices if (*a_events) { if (auto device = (*a_events)->GetDevice()) { - if (globals::game::isVR) { - // In VR, block mouse/keyboard input when menu is open (like Flatrim) - // Allow gamepad input to pass through - // Also handle VR controller devices based on OpenVR compatibility - bool isVRController = ((device == RE::INPUT_DEVICES::INPUT_DEVICE::kVivePrimary) || - (device == RE::INPUT_DEVICES::INPUT_DEVICE::kViveSecondary) || - (device == RE::INPUT_DEVICES::INPUT_DEVICE::kOculusPrimary) || - (device == RE::INPUT_DEVICES::INPUT_DEVICE::kOculusSecondary) || - (device == RE::INPUT_DEVICES::INPUT_DEVICE::kWMRPrimary) || - (device == RE::INPUT_DEVICES::INPUT_DEVICE::kWMRSecondary)); - - // Allow gamepad input to pass through always - if (device == RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad) { - blockedDevice = false; - } - // For VR controllers, only block if OpenVR is compatible - else if (isVRController) { - blockedDevice = globals::features::vr.IsOpenVRCompatible(); - } - // For mouse/keyboard and other devices, block them (like Flatrim) - else { - blockedDevice = true; - } - } else { // Block all devices except gamepad when menu is open blockedDevice = (device != RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad); - } } } } @@ -880,14 +854,12 @@ namespace Hooks */ void Install() { - if (!REL::Module::IsVR()) { - logger::info("Hooking BSImageSpace::Init::IBLF"); - stl::detour_thunk(REL::RelocationID(100480, 107198)); - } + logger::info("Hooking BSImageSpace::Init::IBLF"); + stl::detour_thunk(REL::RelocationID(100480, 107198)); // This input hook also drives per-frame Reflex update (see BSInputDeviceManager_PollInputDevices::thunk). logger::info("Hooking BSInputDeviceManager::PollInputDevices"); - stl::write_thunk_call(REL::RelocationID(67315, 68617).address() + REL::Relocate(0x7B, 0x7B, 0x81)); + stl::write_thunk_call(REL::RelocationID(67315, 68617).address() + REL::Relocate(0x7B, 0x7B)); logger::info("Hooking BSShader::LoadShaders"); stl::detour_thunk(REL::RelocationID(101339, 108326)); @@ -911,19 +883,19 @@ namespace Hooks stl::detour_thunk(REL::RelocationID(100458, 107175)); logger::info("Hooking BSShaderRenderTargets::Create::CreateRenderTarget(s)"); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3, 0x548)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409, 0x55E)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x41C, 0x41F, 0x574)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B, 0x5B0)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x3F0, 0x3F3)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x406, 0x409)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x41C, 0x41F)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x458, 0x45B)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502, 0x661)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19, 0xE06)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B, 0x1917)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25, 0xCD2)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59, 0xD13)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59)); #ifdef TRACY_ENABLE stl::write_thunk_call(REL::RelocationID(35551, 36544).address() + REL::Relocate(0x11F, 0x160)); @@ -940,7 +912,7 @@ namespace Hooks stl::detour_thunk(REL::RelocationID(75532, 77329)); logger::info("Hooking TESWaterReflections::Update_Actor::GetLOSPosition for Sky Reflection Fix"); - stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA, 0x1ed)); + stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA)); logger::info("Hooking Sky::UpdateColors"); stl::detour_thunk(REL::RelocationID(25686, 26233)); @@ -961,9 +933,6 @@ namespace Hooks if (REL::Module::IsAE()) { std::uint8_t patch[] = { 0x41, 0x83, 0xE7, 0x00 }; // and r15d, 0 REL::safe_write(setupGeometryUpdateRenderSpace + 0x71, patch, sizeof(patch)); - } else if (REL::Module::IsVR()) { - std::uint8_t patch[] = { 0x41, 0x83, 0xE4, 0x00 }; // and r12d, 0 - REL::safe_write(setupGeometryUpdateRenderSpace + 0x65, patch, sizeof(patch)); } else { std::uint8_t patch1[] = { 0xB8, 0x00, 0x00 }; // mov eax, 0 REL::safe_write(setupGeometryUpdateRenderSpace + 0x73, patch1, sizeof(patch1)); @@ -976,7 +945,7 @@ namespace Hooks } } - stl::write_thunk_call(REL::RelocationID(100565, 107300).address() + REL::Relocate(0x523, 0xB0E, 0x5FE)); + stl::write_thunk_call(REL::RelocationID(100565, 107300).address() + REL::Relocate(0x523, 0xB0E)); } void InstallEarlyHooks() @@ -987,6 +956,6 @@ namespace Hooks } logger::info("Hooking CreateDXGIFactory"); - *(uintptr_t*)&ptrCreateDXGIFactory = SKSE::PatchIAT(hk_CreateDXGIFactory, "dxgi.dll", !REL::Module::IsVR() ? "CreateDXGIFactory" : "CreateDXGIFactory1"); + *(uintptr_t*)&ptrCreateDXGIFactory = SKSE::PatchIAT(hk_CreateDXGIFactory, "dxgi.dll", "CreateDXGIFactory"); } } diff --git a/src/Menu.cpp b/src/Menu.cpp index 719c90f0a3..284c459d97 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -47,7 +47,6 @@ #include "Features/PerformanceOverlay/ABTesting/ABTestAggregator.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/ScreenshotFeature.h" -#include "Features/VR.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -936,7 +935,7 @@ void Menu::DrawFooter() * callbacks for input processing, settings rendering, and key mapping. This method * serves as the bridge between Menu's state and the extracted overlay rendering logic. * - * Handles VR setup, input event processing, shader compilation status, feature overlays, + * Handles input event processing, shader compilation status, feature overlays, * A/B testing, and ImGui frame management through the specialized renderer component. */ void Menu::DrawOverlay() @@ -992,11 +991,10 @@ void Menu::DrawOverlay() } /** - * @brief Processes queued input events for both VR and non-VR devices + * @brief Processes queued input events * - * This method handles the complex logic of routing input events to appropriate handlers: - * - VR controller events are forwarded to the VR system for specialized processing - * - Non-VR events (keyboard, mouse) are processed directly for ImGui integration + * This method handles the logic of routing input events to appropriate handlers: + * - Keyboard and mouse events are processed directly for ImGui integration * - Includes key state normalization and stuck key detection/correction * * The method maintains thread safety through mutex protection of the input event queue. @@ -1029,28 +1027,7 @@ void Menu::ProcessInputEventQueue() { std::unique_lock mutex(_inputEventMutex); ImGuiIO& io = ImGui::GetIO(); - // Split the queue into VR and non-VR events - std::vector vrEvents; - std::vector nonVREvents; for (auto& event : _keyEventQueue) { - bool isVRController = ((event.device == RE::INPUT_DEVICE::kVivePrimary || event.device == RE::INPUT_DEVICE::kViveSecondary || - event.device == RE::INPUT_DEVICE::kOculusPrimary || event.device == RE::INPUT_DEVICE::kOculusSecondary || - event.device == RE::INPUT_DEVICE::kWMRPrimary || event.device == RE::INPUT_DEVICE::kWMRSecondary)); - - if (globals::features::vr.IsOpenVRCompatible() && isVRController) { - vrEvents.push_back(event); - } else { - nonVREvents.push_back(event); - } - } - // Process VR events in VR - if (!vrEvents.empty()) { - globals::features::vr.ProcessVREvents(vrEvents); - globals::features::vr.UpdateOverlayMenuStateFromInput(); - } - - // Process non-VR events in Menu - for (auto& event : nonVREvents) { if (event.eventType == RE::INPUT_EVENT_TYPE::kChar) { io.AddInputCharacter(event.keyCode); continue; diff --git a/src/Menu.h b/src/Menu.h index 61079df656..e7b39fe566 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -238,7 +238,7 @@ class Menu float FontSize = ThemeManager::Constants::DEFAULT_FONT_SIZE; std::string FontName = "Jost/Jost-Regular.ttf"; // Default font file name (legacy) - float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential + float GlobalScale = 0.f; // exponential std::array(FontRole::Count)> FontRoles = []() { std::array(FontRole::Count)> roles{}; auto setRole = [&roles](FontRole role, std::string family, std::string style, std::string file, float sizeScale) { @@ -499,8 +499,6 @@ class Menu [[nodiscard]] constexpr bool IsHeld() const noexcept { return IsPressed() && IsRepeating(); } [[nodiscard]] constexpr bool IsUp() const noexcept { return (value == 0.0F) && IsRepeating(); } }; - // VR overlay input and cursor helpers - void ProcessVROverlayInput(); private: Settings settings; diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index 5547f56756..b7875751d4 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -574,11 +574,6 @@ void HomePageRenderer::RenderFirstTimeSetupDialog() bool HomePageRenderer::ShouldShowFirstTimeSetup() { - // Never show first-time setup in VR mode - if (REL::Module::IsVR()) { - return false; - } - // Check if already completed this session if (isFirstTimeSetupShown) { return false; diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 58471ac9ef..4a1ee491e4 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -24,8 +24,6 @@ #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" -#include "Features/VR.h" - namespace { std::unordered_map s_windowOverlapAlpha; @@ -133,13 +131,8 @@ void OverlayRenderer::RenderOverlay( float& cachedFontSize, float currentFontSize) { - HandleVRSetup(); processInputEventQueue(); - if (globals::features::vr.IsOpenVRCompatible()) { - globals::features::vr.ProcessControllerInputForImGui(); - } - if (ShouldSkipRendering()) { auto& io = ImGui::GetIO(); io.ClearInputKeys(); @@ -184,13 +177,6 @@ void OverlayRenderer::RenderOverlay( FinalizeImGuiFrame(); } -void OverlayRenderer::HandleVRSetup() -{ - if (globals::features::vr.IsOpenVRCompatible()) { - globals::features::vr.RecreateOverlayTexturesIfNeeded(); - } -} - bool OverlayRenderer::ShouldSkipRendering() { auto shaderCache = globals::shaderCache; @@ -391,9 +377,6 @@ void OverlayRenderer::FinalizeImGuiFrame() ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - if (globals::features::vr.IsOpenVRCompatible()) { - globals::features::vr.SubmitOverlayFrame(); - } } void OverlayRenderer::RenderFirstTimeSetupOverlay() diff --git a/src/Menu/OverlayRenderer.h b/src/Menu/OverlayRenderer.h index d78e908319..b9b9dc6720 100644 --- a/src/Menu/OverlayRenderer.h +++ b/src/Menu/OverlayRenderer.h @@ -10,7 +10,7 @@ class Menu; * @brief Specialized renderer component for overlay and frame management * * This class was extracted from Menu.cpp to handle all overlay-related rendering - * responsibilities including VR setup, shader compilation status, feature overlays, + * responsibilities including shader compilation status, feature overlays, * A/B testing, and ImGui frame lifecycle management. * * The renderer uses a callback-based architecture to maintain separation of concerns @@ -25,7 +25,7 @@ class OverlayRenderer /** * @brief Main overlay rendering entry point * - * Coordinates all overlay rendering activities including VR setup, input processing, + * Coordinates all overlay rendering activities including input processing, * shader compilation status display, feature overlays, A/B testing, and ImGui frame * management. Uses callback functions to access Menu functionality while maintaining * architectural separation. @@ -46,7 +46,6 @@ class OverlayRenderer float currentFontSize); private: - static void HandleVRSetup(); static bool ShouldSkipRendering(); static void HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize); static void InitializeImGuiFrame(Menu& menu); diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 804c7c6ea8..02e6c80882 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -7,7 +7,6 @@ #include "BackgroundBlur.h" #include "Features/ScreenshotFeature.h" -#include "Features/VR.h" #include "Fonts.h" #include "Globals.h" #include "I18n/I18n.h" diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index b43784f8c6..87fb28acfd 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -28,8 +28,6 @@ #include "../Util.h" #include "../Utils/FileSystem.h" #include "../Utils/UI.h" -#include "Features/VR.h" - using namespace SKSE; namespace @@ -1020,12 +1018,9 @@ float ThemeManager::ResolveFontSize(const Menu& menu) // Compute dynamic size from screen resolution float dynamicSize; - if (globals::game::isVR) { - // VR: use overlay height - dynamicSize = VR::Config::kOverlayHeight * Constants::DEFAULT_FONT_RATIO; - } else if (globals::state && globals::state->screenSize.y > 0) { - // Non-VR: use current screen height - dynamicSize = globals::state->screenSize.y * Constants::DEFAULT_FONT_RATIO; + if (globals::game::graphicsState && globals::game::graphicsState->screenHeight > 0) { + // Use current screen height + dynamicSize = (float)globals::game::graphicsState->screenHeight * Constants::DEFAULT_FONT_RATIO; } else { // Fallback: use default font size logger::warn("ThemeManager::ResolveFontSize() - Falling back to Constants::DEFAULT_FONT_SIZE due to missing screen height."); diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index 38bd325651..2ee5f53f4b 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -762,12 +762,12 @@ namespace SIE { "SplitDistance", lightingPSConstants.SplitDistance }, { "SSRParams", lightingPSConstants.SSRParams }, { "WorldMapOverlayParametersPS", lightingPSConstants.WorldMapOverlayParametersPS }, - { "ShadowSampleParam", lightingPSConstants.ShadowSampleParam }, // VR only - { "EndSplitDistances", lightingPSConstants.EndSplitDistances }, // VR only - { "StartSplitDistances", lightingPSConstants.StartSplitDistances }, // VR only - { "DephBiasParam", lightingPSConstants.DephBiasParam }, // VR only - { "ShadowLightParam", lightingPSConstants.ShadowLightParam }, // VR only - { "ShadowMapProj", lightingPSConstants.ShadowMapProj }, // VR only + { "ShadowSampleParam", lightingPSConstants.ShadowSampleParam }, + { "EndSplitDistances", lightingPSConstants.EndSplitDistances }, + { "StartSplitDistances", lightingPSConstants.StartSplitDistances }, + { "DephBiasParam", lightingPSConstants.DephBiasParam }, + { "ShadowLightParam", lightingPSConstants.ShadowLightParam }, + { "ShadowMapProj", lightingPSConstants.ShadowMapProj }, { "AmbientColor", lightingPSConstants.AmbientColor }, { "FogColor", lightingPSConstants.FogColor }, { "ColourOutputClamp", lightingPSConstants.ColourOutputClamp }, @@ -786,8 +786,8 @@ namespace SIE { "LandscapeTexture5to6IsSpecPower", lightingPSConstants.LandscapeTexture5to6IsSpecPower }, { "SnowRimLightParameters", lightingPSConstants.SnowRimLightParameters }, { "CharacterLightParams", lightingPSConstants.CharacterLightParams }, - { "InvWorldMat", lightingPSConstants.InvWorldMat }, // VR only - { "PreviousWorldMat", lightingPSConstants.PreviousWorldMat }, // VR only + { "InvWorldMat", lightingPSConstants.InvWorldMat }, + { "PreviousWorldMat", lightingPSConstants.PreviousWorldMat }, { "PBRFlags", lightingPSConstants.PBRFlags }, { "PBRParams1", lightingPSConstants.PBRParams1 }, @@ -879,11 +879,7 @@ namespace SIE { "ScaleMask", 13 }, }; - if (REL::Module::IsVR()) { - grassVS.insert({ "Padding", 14 }); - } else { - grassVS.insert({ "ShadowClampValue", 14 }); - } + grassVS.insert({ "ShadowClampValue", 14 }); const auto& grassPSConstants = ShaderConstants::GrassPS::Get(); @@ -985,14 +981,12 @@ namespace SIE { "CellTexCoordOffset", 11 }, }; - if (!REL::Module::IsVR()) { - waterVS.insert( - { - { "SubTexOffset", 12 }, - { "PosAdjust", 13 }, - { "MatProj", 14 }, - }); - } + waterVS.insert( + { + { "SubTexOffset", 12 }, + { "PosAdjust", 13 }, + { "MatProj", 14 }, + }); auto& waterPS = result[static_cast(RE::BSShader::Type::Water)] [static_cast(ShaderClass::Pixel)]; @@ -1052,26 +1046,14 @@ namespace SIE { "ShadowLightParam", 8 }, }; - if (!REL::Module::IsVR()) { - utilityPS.insert( - { - { "ShadowFadeParam", 9 }, - { "VPOSOffset", 10 }, - { "EndSplitDistances", 11 }, - { "StartSplitDistances", 12 }, - { "FocusShadowFadeParam", 13 }, - }); - } else { - utilityPS.insert( - { - { "StereoClipRects", 9 }, // VR only - { "ShadowFadeParam", 10 }, - { "VPOSOffset", 11 }, - { "EndSplitDistances", 12 }, - { "StartSplitDistances", 13 }, - { "FocusShadowFadeParam", 14 }, - }); - } + utilityPS.insert( + { + { "ShadowFadeParam", 9 }, + { "VPOSOffset", 10 }, + { "EndSplitDistances", 11 }, + { "StartSplitDistances", 12 }, + { "FocusShadowFadeParam", 13 }, + }); return result; } @@ -1427,8 +1409,6 @@ namespace SIE defines[lastIndex++] = { "D3DCOMPILE_SKIP_OPTIMIZATION", nullptr }; defines[lastIndex++] = { "D3DCOMPILE_DEBUG", nullptr }; } - if (REL::Module::IsVR()) - defines[lastIndex++] = { "VR", nullptr }; auto shaderDefines = globals::state->GetDefines(); if (!shaderDefines->empty()) { for (unsigned int i = 0; i < shaderDefines->size(); i++) @@ -1792,16 +1772,9 @@ namespace SIE { "BSImagespaceShaderVolumetricLightingBlurHCS", RE::ImageSpaceManager::GetCurrentIndex(ISVolumetricLightingBlurHCS) }, { "BSImagespaceShaderVolumetricLightingBlurVCS", RE::ImageSpaceManager::GetCurrentIndex(ISVolumetricLightingBlurVCS) }, - // VR only shaders - // Disable BSImagespaceShaderCopyDepthBuffer since we don't have it REed and it causes issues with cache and upscaling - // https://github.com/doodlum/skyrim-community-shaders/issues/1552 - // { "BSImagespaceShaderCopyDepthBuffer", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer) }, - // { "BSImagespaceShaderCopyDepthBuffer_DR", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBuffer_DR) }, - // { "BSImagespaceShaderCopyDepthBufferTargetSize", RE::ImageSpaceManager::GetCurrentIndex(ISCopyDepthBufferTargetSize) }, { "BSImagespaceShaderGraphicsTextureFilterMode", RE::ImageSpaceManager::GetCurrentIndex(ISGraphicsTextureFilterMode) }, { "BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDownsampleHierarchicalDepthBufferCS) }, { "BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDiffScaleDownsampleDepthBufferCS) }, - { "BSImagespaceShaderISFullScreenVR", RE::ImageSpaceManager::GetCurrentIndex(ISFullScreenVR) }, { "BSImagespaceShaderISTransformLvl7PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISTransformLvl7PreTest) }, { "BSImagespaceShaderISLvl6PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl6PreTest) }, { "BSImagespaceShaderISLvl5PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl5PreTest) }, @@ -1839,9 +1812,6 @@ namespace SIE } auto state = globals::state; - if (globals::game::isVR && strcmp(shader.fxpFilename, "OBBOcclusionTesting") == 0) - // use vanilla shader - return nullptr; if (!((ShaderCache::IsSupportedShader(shader) || state->IsDeveloperMode() && state->IsShaderEnabled(shader)) && state->enableVShaders)) { return nullptr; @@ -1883,9 +1853,6 @@ namespace SIE uint32_t descriptor) { auto state = globals::state; - if (globals::game::isVR && strcmp(shader.fxpFilename, "OBBOcclusionTesting") == 0) - // use vanilla shader - return nullptr; if (!((ShaderCache::IsSupportedShader(shader) || state->IsDeveloperMode() && state->IsShaderEnabled(shader)) && state->enablePShaders)) { return nullptr; diff --git a/src/ShaderCache.h b/src/ShaderCache.h index 3808a10236..15feb124ad 100644 --- a/src/ShaderCache.h +++ b/src/ShaderCache.h @@ -14,63 +14,10 @@ namespace ShaderConstants { static const LightingPS& Get() { - static LightingPS instance = REL::Module::IsVR() ? GetVR() : GetFlat(); + static LightingPS instance{}; return instance; } - static LightingPS GetFlat() - { - return LightingPS{}; - } - - static LightingPS GetVR() - { - return LightingPS{ - .AmbientColor = 24, - .FogColor = 25, - .ColourOutputClamp = 26, - .EnvmapData = 27, - .ParallaxOccData = 28, - .TintColor = 29, - .LODTexParams = 30, - .SpecularColor = 31, - .SparkleParams = 32, - .MultiLayerParallaxData = 33, - .LightingEffectParams = 34, - .IBLParams = 35, - .LandscapeTexture1to4IsSnow = 36, - .LandscapeTexture5to6IsSnow = 37, - .LandscapeTexture1to4IsSpecPower = 38, - .LandscapeTexture5to6IsSpecPower = 39, - .SnowRimLightParameters = 40, - .CharacterLightParams = 41, - .PBRFlags = 44, - .PBRParams1 = 45, - .LandscapeTexture2PBRParams = 46, - .LandscapeTexture3PBRParams = 47, - .LandscapeTexture4PBRParams = 48, - .LandscapeTexture5PBRParams = 49, - .LandscapeTexture6PBRParams = 50, - .PBRParams2 = 51, - .LandscapeTexture1GlintParameters = 52, - .LandscapeTexture2GlintParameters = 53, - .LandscapeTexture3GlintParameters = 54, - .LandscapeTexture4GlintParameters = 55, - .LandscapeTexture5GlintParameters = 56, - .LandscapeTexture6GlintParameters = 57, - .MaterialObjectRGBScale = 58, // RGB multipliers for material objects - - .ShadowSampleParam = 18, - .EndSplitDistances = 19, - .StartSplitDistances = 20, - .DephBiasParam = 21, - .ShadowLightParam = 22, - .ShadowMapProj = 23, - .InvWorldMat = 42, - .PreviousWorldMat = 43, - }; - } - const int32_t NumLightNumShadowLight = 0; const int32_t PointLightPosition = 1; const int32_t PointLightColor = 2; @@ -138,20 +85,10 @@ namespace ShaderConstants { static const GrassPS& Get() { - static GrassPS instance = REL::Module::IsVR() ? GetVR() : GetFlat(); + static GrassPS instance{}; return instance; } - static GrassPS GetFlat() - { - return GrassPS{}; - } - - static GrassPS GetVR() - { - return GrassPS{}; - } - const int32_t PBRFlags = 0; const int32_t PBRParams1 = 1; const int32_t PBRParams2 = 2; @@ -161,20 +98,10 @@ namespace ShaderConstants { static const EffectPS& Get() { - static EffectPS instance = REL::Module::IsVR() ? GetVR() : GetFlat(); + static EffectPS instance{}; return instance; } - static EffectPS GetFlat() - { - return EffectPS{}; - } - - static EffectPS GetVR() - { - return EffectPS{}; - } - const int32_t PropertyColor = 0; const int32_t AlphaTestRef = 1; const int32_t MembraneRimColor = 2; @@ -375,17 +302,6 @@ namespace SIE inline static bool IsSupportedShader(const RE::BSShader::Type type) { - if (!REL::Module::IsVR()) - return type == RE::BSShader::Type::Lighting || - type == RE::BSShader::Type::BloodSplatter || - type == RE::BSShader::Type::DistantTree || - type == RE::BSShader::Type::Sky || - type == RE::BSShader::Type::Grass || - type == RE::BSShader::Type::Particle || - type == RE::BSShader::Type::Water || - type == RE::BSShader::Type::Effect || - type == RE::BSShader::Type::Utility || - type == RE::BSShader::Type::ImageSpace; return type == RE::BSShader::Type::Lighting || type == RE::BSShader::Type::BloodSplatter || type == RE::BSShader::Type::DistantTree || diff --git a/src/State.cpp b/src/State.cpp index 2a7a79e65d..9e43113329 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -17,7 +17,6 @@ #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" -#include "Features/VRStereoOptimizations.h" #include "Features/VolumetricShadows.h" #include "Menu.h" #include "SceneSettingsManager.h" @@ -201,7 +200,7 @@ void State::Reset() frameCount++; if (auto* imageSpaceManager = RE::ImageSpaceManager::GetSingleton()) { - GET_INSTANCE_MEMBER(BSImagespaceShaderApplyReflections, imageSpaceManager); + auto& BSImagespaceShaderApplyReflections = imageSpaceManager->GetRuntimeData().BSImagespaceShaderApplyReflections; // Disable reflections being applied to things other than water if (BSImagespaceShaderApplyReflections.get()) { @@ -210,9 +209,7 @@ void State::Reset() } // Disable "improved" snow shader, unsupported - if (!globals::game::isVR) { - RE::GetINISetting("bEnableImprovedSnow:Display")->data.b = false; - } + RE::GetINISetting("bEnableImprovedSnow:Display")->data.b = false; activeReflections = false; } @@ -725,7 +722,6 @@ void State::CheckTypedUAVLoadSupport() { DXGI_FORMAT_R16G16B16A16_FLOAT, "R16G16B16A16_FLOAT", "Dynamic Cubemaps (HDR), Skylighting outProbeArray" }, { DXGI_FORMAT_R16G16B16A16_UNORM, "R16G16B16A16_UNORM", "Grass Collision (collisionTexture)" }, { DXGI_FORMAT_R16G16_UNORM, "R16G16_UNORM", "Terrain Shadows (RWTexShadowHeights)" }, - { DXGI_FORMAT_R16G16_FLOAT, "R16G16_FLOAT", "VR Stereo Blend (kMOTION_VECTOR reprojection)" }, { DXGI_FORMAT_R8G8B8A8_UNORM, "R8G8B8A8_UNORM", "HDR Display UI brightness (uiTexture)" }, { DXGI_FORMAT_R8_UINT, "R8_UINT", "Skylighting accumulation frames (outAccumFramesArray)" }, { DXGI_FORMAT_R16_FLOAT, "R16_FLOAT", "Vanilla volumetric lighting density (DensityRW)" }, @@ -755,7 +751,7 @@ void State::CheckTypedUAVLoadSupport() logger::warn( "[TypedUAVLoad] One or more required formats lack typed-UAV-load support on this GPU. " "Affected features will read undefined data and may produce visual artifacts. " - "Consider disabling: Dynamic Cubemaps, Grass Collision, Terrain Shadows, Skylighting, HDR Display, VR Stereo Optimisations."); + "Consider disabling: Dynamic Cubemaps, Grass Collision, Terrain Shadows, Skylighting, HDR Display."); } } @@ -782,11 +778,9 @@ void State::SetupResources() featureDataCB = new ConstantBuffer(ConstantBufferDesc((uint32_t)size)); // Grab main texture to get resolution - // VR cannot use viewport->screenWidth/Height as it's the desktop preview window's resolution and not HMD D3D11_TEXTURE2D_DESC texDesc{}; renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].texture->GetDesc(&texDesc); - screenSize = { (float)texDesc.Width, (float)texDesc.Height }; globals::d3d::context->QueryInterface(__uuidof(pPerf), reinterpret_cast(&pPerf)); featureLevel = globals::d3d::device->GetFeatureLevel(); @@ -970,14 +964,14 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.DirLightColor *= lightRuntimeData.fade; auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - data.DirLightColor *= !globals::game::isVR ? imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale : imageSpaceManager->GetVRRuntimeData().data.baseData.hdr.sunlightScale; + data.DirLightColor *= imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale; const auto& direction = dirLight->GetWorldDirection(); data.DirLightDirection = { -direction.x, -direction.y, -direction.z, 0.0f }; data.DirLightDirection.Normalize(); data.CameraData = Util::GetCameraData(); - data.BufferDim = { screenSize.x, screenSize.y, 1.0f / screenSize.x, 1.0f / screenSize.y }; + data.BufferDim = { (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight, 1.0f / (float)globals::game::graphicsState->screenWidth, 1.0f / (float)globals::game::graphicsState->screenHeight }; data.Timer = timer; auto temporal = Util::GetTemporal(); @@ -994,23 +988,7 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b } } - // Fallback water height for the VR analytical mask when tile 12 returns the sentinel. - // Uses player->GetWaterHeight() (reads relevantWaterHeight from LOADED_REF_DATA) gated by - // underwaterCount > 0 so it is only set when the player is actually in a water body. - // Covers both interior water (where TES::GetWaterHeight returns -NI_INFINITY) and exterior - // partial submersion. Stored as eye-0 camera-relative Z to match WaterData[].w. data.WaterSystemHeight = -RE::NI_INFINITY; - if (globals::game::isVR) { - if (auto player = globals::game::player) { - if (player->loadedData && player->loadedData->underwaterCount > 0) { - float worldHeight = player->GetWaterHeight(); - if (worldHeight > -RE::NI_INFINITY) { - auto eye0Pos = Util::GetEyePosition(0); - data.WaterSystemHeight = worldHeight - eye0Pos.z; - } - } - } - } data.InInterior = Util::IsInterior(); data.HasDirectionalShadows = HasDirectionalShadows(); @@ -1027,8 +1005,9 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b if (upscaling.loaded) { auto upscaleMethod = upscaling.GetUpscaleMethod(); if (temporal && upscaleMethod != Upscaling::UpscaleMethod::kTAA) { - auto renderSize = Util::ConvertToDynamic(screenSize, true); - data.MipBias = std::log2f(renderSize.x / screenSize.x); + float2 screenSz{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto renderSize = Util::ConvertToDynamic(screenSz, true); + data.MipBias = std::log2f(renderSize.x / screenSz.x); if (upscaleMethod == Upscaling::UpscaleMethod::kDLSS) data.MipBias -= 1.0f; } else { diff --git a/src/State.h b/src/State.h index 7057fee387..bf42bd6039 100644 --- a/src/State.h +++ b/src/State.h @@ -257,7 +257,7 @@ class State uint InMapMenu; uint HideSky; float MipBias; - float WaterSystemHeight; // TES::GetWaterHeight at eye-0 in camera-relative Z; -NI_INFINITY when no water body found (VR only) + float WaterSystemHeight; // TES::GetWaterHeight in camera-relative Z; -NI_INFINITY when no water body found float3 pad0; float4 AmbientSHR; float4 AmbientSHG; @@ -276,7 +276,6 @@ class State uint frameCount = 0; // Skyrim constants - float2 screenSize = {}; D3D_FEATURE_LEVEL featureLevel; TracyD3D11Ctx tracyCtx = nullptr; // Tracy context diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 4dc47668d9..530f13f5c5 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -1182,7 +1182,7 @@ bool TruePBR::TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land) if (land->loadedData != nullptr && land->loadedData->mesh[0] != nullptr) { land->data.flags.set(static_cast(8)); for (uint32_t quadIndex = 0; quadIndex < 4; ++quadIndex) { - auto shaderProperty = static_cast(memoryManager->Allocate(REL::Module::IsVR() ? 0x178 : sizeof(RE::BSLightingShaderProperty), 0, false)); + auto shaderProperty = static_cast(memoryManager->Allocate(sizeof(RE::BSLightingShaderProperty), 0, false)); shaderProperty->Ctor(); { diff --git a/src/TruePBR.h b/src/TruePBR.h index 044baea62b..befc8db420 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -19,7 +19,6 @@ struct TruePBR : Feature virtual std::string GetShortName() override { return "TruePBR"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } virtual bool IsCore() const override { return true; } - virtual bool SupportsVR() override { return true; } virtual bool IsInMenu() const override { return true; } virtual bool DrawFailLoadMessage() const override { return false; } diff --git a/src/Utils/D3D.cpp b/src/Utils/D3D.cpp index 5f6c4b5737..e7b4a5ba70 100644 --- a/src/Utils/D3D.cpp +++ b/src/Utils/D3D.cpp @@ -30,7 +30,7 @@ namespace Util { if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < GetRenderTargetCount(); i++) { + for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return rt.SRV; @@ -45,7 +45,7 @@ namespace Util { if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < GetRenderTargetCount(); i++) { + for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return rt.RTV; @@ -62,7 +62,7 @@ namespace Util if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < GetRenderTargetCount(); i++) { + for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return std::string(magic_enum::enum_name(static_cast(i))); @@ -78,7 +78,7 @@ namespace Util using RENDER_TARGET = RE::RENDER_TARGETS::RENDER_TARGET; if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < GetRenderTargetCount(); i++) { + for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return std::string(magic_enum::enum_name(static_cast(i))); @@ -159,8 +159,6 @@ namespace Util } } - if (REL::Module::IsVR()) - macros.push_back({ "VR", "" }); if (globals::state->IsDeveloperMode()) { macros.push_back({ "D3DCOMPILE_SKIP_OPTIMIZATION", "" }); macros.push_back({ "D3DCOMPILE_DEBUG", "" }); diff --git a/src/Utils/D3D.h b/src/Utils/D3D.h index eef88de3f0..8104442dcb 100644 --- a/src/Utils/D3D.h +++ b/src/Utils/D3D.h @@ -17,17 +17,6 @@ namespace Util void ApplyHighlightTintToTexture(ID3D11Texture2D* texture, bool isHighlighted, const std::array& highlightColor = { 1.0f, 0.5f, 0.0f, 0.3f }); HRESULT CreateOverlayTextureAndRTV(ID3D11Device* device, int width, int height, ID3D11Texture2D** outTex, ID3D11RenderTargetView** outRTV); - // VR-aware counts for render targets - inline int GetRenderTargetCount() - { - return REL::Module::IsVR() ? RE::RENDER_TARGETS::kVRTOTAL : RE::RENDER_TARGETS::kTOTAL; - } - - inline int GetDepthStencilCount() - { - return REL::Module::IsVR() ? RE::RENDER_TARGETS_DEPTHSTENCIL::kVRTOTAL : RE::RENDER_TARGETS_DEPTHSTENCIL::kTOTAL; - } - HRESULT SaveTextureToFile(ID3D11Device* device, ID3D11DeviceContext* context, const std::filesystem::path& path, ID3D11Texture2D* tex); HRESULT LoadTextureFromFile(ID3D11Device* device, const std::filesystem::path& path, ID3D11Texture2D** outTex, ID3D11ShaderResourceView** outSRV); diff --git a/src/Utils/Game.cpp b/src/Utils/Game.cpp index 31900f417c..6455a60f1d 100644 --- a/src/Utils/Game.cpp +++ b/src/Utils/Game.cpp @@ -32,7 +32,7 @@ namespace Util { if (globals::game::shadowState) { if (auto tes = RE::TES::GetSingleton()) { - auto position = GetEyePosition(0); + auto position = GetEyePosition(); position.x += offsetX; position.y += offsetY; if (auto cell = tes->GetCell(position)) { @@ -93,29 +93,10 @@ namespace Util return float4(1.0f, 1.0f, 1.0f, -FLT_MAX); } - RE::NiPoint3 GetAverageEyePosition() + RE::NiPoint3 GetEyePosition() { auto shadowState = globals::game::shadowState; - if (!REL::Module::IsVR()) - return shadowState->GetRuntimeData().posAdjust.getEye(); - return (shadowState->GetVRRuntimeData().posAdjust.getEye(0) + shadowState->GetVRRuntimeData().posAdjust.getEye(1)) * 0.5f; - } - - RE::NiPoint3 GetEyePosition(int eyeIndex) - { - auto shadowState = globals::game::shadowState; - if (!REL::Module::IsVR()) - return shadowState->GetRuntimeData().posAdjust.getEye(); - return shadowState->GetVRRuntimeData().posAdjust.getEye(eyeIndex); - } - - RE::BSGraphics::ViewData GetCameraData(int eyeIndex) - { - auto shadowState = globals::game::shadowState; - if (!REL::Module::IsVR()) { - return shadowState->GetRuntimeData().cameraData.getEye(); - } - return shadowState->GetVRRuntimeData().cameraData.getEye(eyeIndex); + return shadowState->GetRuntimeData().posAdjust.getEye(); } float4 GetCameraData() @@ -135,7 +116,7 @@ namespace Util bool GetTemporal() { auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - return (!REL::Module::IsVR() ? imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA->taaEnabled : imageSpaceManager->GetVRRuntimeData().BSImagespaceShaderISTemporalAA->taaEnabled); + return imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA->taaEnabled; } float GetVerticalFOVRad() @@ -143,7 +124,7 @@ namespace Util static float& cameraFOVDeg = (*(float*)(REL::RelocationID(513786, 388785).address())); // FOV degrees float hFOVRad = cameraFOVDeg * (3.14159265359f / 180.0f); float unitHalfWidth = tan(hFOVRad / 2); // This is same as camera frustum RL - float unitHalfHeight = unitHalfWidth / (globals::state->screenSize.x / globals::state->screenSize.y); // frustum TB + float unitHalfHeight = unitHalfWidth / ((float)globals::game::graphicsState->screenWidth / (float)globals::game::graphicsState->screenHeight); // frustum TB float vFOVRad = 2.0f * atan(unitHalfHeight); return vFOVRad; } @@ -163,7 +144,7 @@ namespace Util DispatchCount GetScreenDispatchCount(bool a_dynamic) { - float2 resolution = globals::state->screenSize; + float2 resolution{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; if (a_dynamic) ConvertToDynamic(resolution); diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 83f620cdc7..fab94ced16 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -2,32 +2,6 @@ #pragma once -/** - @def GET_INSTANCE_MEMBER - @brief Set variable in current namespace based on instance member from GetRuntimeData or GetVRRuntimeData. - - @warning The class must have both a GetRuntimeData() and GetVRRuntimeData() function. - - @param a_value The instance member value to access (e.g., renderTargets). - @param a_source The instance of the class (e.g., state). - @result The a_value will be set as a variable in the current namespace. (e.g., auto& renderTargets = state->renderTargets;) - */ -#define GET_INSTANCE_MEMBER(a_value, a_source) \ - auto& a_value = !REL::Module::IsVR() ? a_source->GetRuntimeData().a_value : a_source->GetVRRuntimeData().a_value; - -/** - @def GET_INSTANCE_MEMBER_PTR - @brief Return refptr to runtimedata in current namespace based on instance member from GetRuntimeData or GetVRRuntimeData. - - @warning The class must have both a GetRuntimeData() and GetVRRuntimeData() function. - - @param a_value The instance member value to access (e.g., renderTargets). - @param a_source The instance of the class (e.g., state). - @result The a_value will be returned as a refptr. (e.g., &state->renderTargets;) - */ -#define GET_INSTANCE_MEMBER_PTR(a_value, a_source) \ - &(!REL::Module::IsVR() ? a_source->GetRuntimeData().a_value : a_source->GetVRRuntimeData().a_value) - namespace Util { void StoreTransform3x4NoScale(DirectX::XMFLOAT3X4& Dest, const RE::NiTransform& Source); @@ -37,9 +11,7 @@ namespace Util bool GetTemporal(); float GetVerticalFOVRad(); - RE::NiPoint3 GetAverageEyePosition(); - RE::NiPoint3 GetEyePosition(int eyeIndex); - RE::BSGraphics::ViewData GetCameraData(int eyeIndex); + RE::NiPoint3 GetEyePosition(); float2 ConvertToDynamic(float2 a_size, bool a_ignoreLock = false); diff --git a/src/Utils/GameSetting.h b/src/Utils/GameSetting.h index dff7d2f3fe..b212c9ebbc 100644 --- a/src/Utils/GameSetting.h +++ b/src/Utils/GameSetting.h @@ -107,7 +107,7 @@ namespace Util * @brief Gets the value of a game setting, transparently handling INI-based and offset-based settings. * * For INI-based settings (offset == 0), tries INISettingCollection, INIPrefSettingCollection, - * then GameSettingCollection. For offset-based settings (e.g., VR where INI entries don't exist), + * then GameSettingCollection. For offset-based settings, * reads directly from the memory address. * * @tparam T The expected value type (bool, float, std::int32_t, or std::uint32_t). @@ -152,7 +152,7 @@ namespace Util * @brief Sets the value of a game setting, transparently handling INI-based and offset-based settings. * * For INI-based settings (offset == 0), tries INISettingCollection, INIPrefSettingCollection, - * then GameSettingCollection. For offset-based settings (e.g., VR where INI entries don't exist), + * then GameSettingCollection. For offset-based settings, * writes directly to the memory address. * * @tparam T The value type (bool, float, std::int32_t, or std::uint32_t). diff --git a/src/Utils/Input.h b/src/Utils/Input.h index d4d01aff61..7e5ba6d768 100644 --- a/src/Utils/Input.h +++ b/src/Utils/Input.h @@ -7,17 +7,15 @@ /** * @brief Identifies the type of input device for input mapping - * - * Used to distinguish between VR controllers, keyboard, mouse, and gamepads. */ enum class InputDeviceType { - Primary = 0, ///< VR: The dominant hand controller (right for right-handed, left for left-handed) - Secondary = 1, ///< VR: The non-dominant hand controller - Both = 2, ///< VR: Both controllers simultaneously + Primary = 0, + Secondary = 1, + Both = 2, Keyboard = 3, ///< Keyboard input Mouse = 4, ///< Mouse input - Gamepad = 5 ///< Gamepad/Controller input (non-VR) + Gamepad = 5 ///< Gamepad/Controller input }; /** @@ -63,9 +61,9 @@ constexpr bool IsValidDevice(InputDeviceType device) * The upper 16 bits store the device type, lower 16 bits store the key code. * * Can represent: - * - VR Controller button presses * - Keyboard key presses * - Mouse button clicks + * - Gamepad button presses */ struct InputCombo { @@ -83,12 +81,7 @@ struct InputCombo { } - // VR helper methods - static InputCombo Primary(uint32_t key) { return InputCombo(InputDeviceType::Primary, key); } - static InputCombo Secondary(uint32_t key) { return InputCombo(InputDeviceType::Secondary, key); } - static InputCombo Both(uint32_t key) { return InputCombo(InputDeviceType::Both, key); } - - // Desktop helper methods + // Helper methods static InputCombo Keyboard(uint32_t key) { return InputCombo(InputDeviceType::Keyboard, key); } static InputCombo Mouse(uint32_t key) { return InputCombo(InputDeviceType::Mouse, key); } static InputCombo Gamepad(uint32_t key) { return InputCombo(InputDeviceType::Gamepad, key); } @@ -192,84 +185,12 @@ struct InputCombo * If so, we could serialize as a list of integers (key codes) for better readability. */ - /** - * @brief Static helper to get a formatted string for a VR combo - */ - static std::string GetVRString(const std::vector& combo) - { - std::string result; - for (size_t i = 0; i < combo.size(); ++i) { - if (i > 0) - result += " + "; - const auto& input = combo[i]; - - if (input.GetDevice() == InputDeviceType::Keyboard) { - result += std::format("Key:{:X}", input.GetKey()); - } else { - // VR Button mapping - // Based on BSOpenVRControllerDevice::Keys enum values - // 2 = Grip, 7 = Trigger, 32 = Touchpad, 33 = Stick, 1 = BY, 31 = XA - // These are standard OpenVR / SkyrimVR constants - switch (input.GetKey()) { - case 2: - result += "Grip"; - break; - case 7: - result += "Trigger"; - break; - case 32: - result += "Touchpad"; - break; - case 33: - result += "Stick"; - break; - case 1: - result += "B/Y"; - break; - case 31: - result += "A/X"; - break; - case 9: - result += "Menu"; - break; - case 34: - result += "Shoulder"; - break; - default: - result += std::format("Btn:{:d}", input.GetKey()); - break; - } - - // Append device info if mixed or specific - switch (input.GetDevice()) { - case InputDeviceType::Primary: - result += "(Pri)"; - break; - case InputDeviceType::Secondary: - result += "(Sec)"; - break; - case InputDeviceType::Both: - result += "(Both)"; - break; - default: - break; - } - } - } - - if (result.empty()) { - return "None"; - } - - return result; - } - /** * @brief Wrapper for std::vector to provide custom JSON serialization. * * Serialization rules for backward compatibility: * - Single keyboard key (no modifiers): saves as plain uint32_t key code - * - Single VR/controller input: saves as plain uint32_t packed value + * - Single controller input: saves as plain uint32_t packed value * - Multiple keys (combo): saves as array of uint32_t values * - Empty: saves as 0 (unbound) * @@ -292,7 +213,7 @@ struct InputCombo // Single keyboard key - save as plain key code j = combos[0].GetKey(); } else { - // Single VR/controller input - save as packed value + // Single controller input - save as packed value j = combos[0].deviceAndKey; } return; @@ -320,7 +241,7 @@ struct InputCombo } j = keyCodes; } else { - // For VR or mixed inputs, use the packed format + // For mixed inputs, use the packed format std::vector packedValues; packedValues.reserve(combos.size()); for (const auto& c : combos) { diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 9e6e6464ea..df59ea4fba 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -25,11 +25,9 @@ #include #include "../Feature.h" -#include "../Features/VR.h" #include "../Globals.h" #include "../Menu.h" #include "FileSystem.h" -#include "VRUtils.h" #define STB_IMAGE_IMPLEMENTATION #include @@ -1883,18 +1881,6 @@ namespace Util if (combo.empty()) return "None"; - bool hasVRInput = false; - for (const auto& input : combo) { - if (input.GetDevice() != InputDeviceType::Keyboard && input.GetDevice() != InputDeviceType::Mouse) { - hasVRInput = true; - break; - } - } - - if (hasVRInput && REL::Module::IsVR()) { - return InputCombo::GetVRString(combo); - } - std::string result; for (size_t i = 0; i < combo.size(); ++i) { if (i > 0) @@ -2431,50 +2417,6 @@ namespace Util } } - // Draw VR-specific color coding if applicable - if (REL::Module::IsVR() && !combo.empty()) { - ImGui::SameLine(); - - // Check if we have mixed devices - bool hasPrimary = false; - bool hasSecondary = false; - bool hasBoth = false; - - for (const auto& input : combo) { - switch (input.GetDevice()) { - case InputDeviceType::Primary: - hasPrimary = true; - break; - case InputDeviceType::Secondary: - hasSecondary = true; - break; - case InputDeviceType::Both: - hasBoth = true; - break; - default: - break; - } - } - - ImVec4 indicatorColor = GetControllerDefaultColor(); - const char* indicatorText = ""; - - if (hasBoth || (hasPrimary && hasSecondary)) { - indicatorColor = GetControllerBothColor(); - indicatorText = hasBoth ? "(Both)" : "(Mixed)"; - } else if (hasPrimary) { - indicatorColor = GetControllerPrimaryColor(); - indicatorText = "(Primary)"; - } else if (hasSecondary) { - indicatorColor = GetControllerSecondaryColor(); - indicatorText = "(Secondary)"; - } - - if (indicatorText[0] != '\0') { - ImGui::TextColored(indicatorColor, "%s", indicatorText); - } - } - return changed; } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index e93babe8d8..89addd6526 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1074,7 +1074,7 @@ namespace Util * @brief Converts a key combo (vector of InputCombo) to a human-readable string * * For keyboard-only combos, produces strings like "Ctrl + Shift + A". - * For VR inputs, delegates to InputCombo::GetVRString for proper formatting. + * For non-keyboard inputs, formats using the packed device+key representation. * * @param combo Vector of InputCombo representing the key combination * @return Human-readable string representation of the combo, or "None" if empty @@ -1516,9 +1516,9 @@ namespace Util } /** - * @brief Unified input recording widget for both VR and Desktop + * @brief Unified input recording widget * - * Handles recording of multi-key sequences for keyboard, mouse, and VR controllers. + * Handles recording of multi-key sequences for keyboard and mouse. * Supports modifiers, combo sequences, and device-specific rendering. * * @param label The label for the input setting diff --git a/src/Utils/VRUtils.cpp b/src/Utils/VRUtils.cpp deleted file mode 100644 index 23ac293156..0000000000 --- a/src/Utils/VRUtils.cpp +++ /dev/null @@ -1,241 +0,0 @@ -#include "VRUtils.h" -#include "Features/VR.h" // For ButtonCombo and ControllerDevice definitions -#include "RE/B/BSOpenVR.h" -#include "UI.h" -#include - -namespace Util -{ - void DrawButtonCombo(const std::vector& combo, bool showControllerLabels) - { - bool anyDrawn = false; - for (size_t i = 0; i < combo.size(); ++i) { - if (combo[i].GetKey() == 0) - continue; - if (i > 0) { - ImGui::SameLine(); - ImGui::Text("+"); - ImGui::SameLine(); - } - ImVec4 color; - switch (combo[i].GetDevice()) { - case InputDeviceType::Primary: - color = Util::GetControllerPrimaryColor(); - break; - case InputDeviceType::Secondary: - color = Util::GetControllerSecondaryColor(); - break; - case InputDeviceType::Both: - color = Util::GetControllerBothColor(); - break; - default: - color = Util::GetControllerDefaultColor(); - break; - } - ImGui::PushStyleColor(ImGuiCol_Text, color); - ImGui::Text("%s", RE::GetOpenVRButtonName(combo[i].GetKey())); - ImGui::PopStyleColor(); - anyDrawn = true; - if (showControllerLabels) { - ImGui::SameLine(); - ImVec4 labelColor = Util::GetControllerDefaultColor(); - const char* label = ""; - switch (combo[i].GetDevice()) { - case InputDeviceType::Primary: - label = "(Primary Controller)"; - labelColor = Util::GetControllerPrimaryColor(); - break; - case InputDeviceType::Secondary: - label = "(Secondary Controller)"; - labelColor = Util::GetControllerSecondaryColor(); - break; - case InputDeviceType::Both: - label = "(Both Controllers)"; - labelColor = Util::GetControllerBothColor(); - break; - default: - break; - } - ImGui::TextColored(labelColor, "%s", label); - if (i < combo.size() - 1) - ImGui::SameLine(); - } - } - if (anyDrawn) { - if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawColoredMultiLineTooltip({ { "Color coding:", Util::GetControllerDefaultColor() }, - { "Yellow = Primary controller", Util::GetControllerPrimaryColor() }, - { "Blue = Secondary controller", Util::GetControllerSecondaryColor() }, - { "Green = Both controllers (Yellow + Blue)", Util::GetControllerBothColor() } }); - } - } - } - - vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ) - { - // Initialize as identity matrix to ensure valid transform on early returns - vr::HmdMatrix34_t transform = {}; - transform.m[0][0] = 1.0f; - transform.m[1][1] = 1.0f; - transform.m[2][2] = 1.0f; - // All other elements remain 0.0f from the {} initialization - - // Use the same OpenVR access pattern as the VR class - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr) - return transform; - - auto* system = openvr->vrSystem; - if (!system) - return transform; - - vr::TrackedDevicePose_t hmdPose; - if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) - return transform; - if (!hmdPose.bPoseIsValid) - return transform; - - transform = hmdPose.mDeviceToAbsoluteTracking; - - // Apply HMD overlay offsets (in HMD local space) - transform.m[0][3] += transform.m[0][0] * offsetX + transform.m[0][1] * offsetY + transform.m[0][2] * offsetZ; - transform.m[1][3] += transform.m[1][0] * offsetX + transform.m[1][1] * offsetY + transform.m[1][2] * offsetZ; - transform.m[2][3] += transform.m[2][0] * offsetX + transform.m[2][1] * offsetY + transform.m[2][2] * offsetZ; - - return transform; - } - - //============================================================================= - // NEW ACTIVE FUNCTIONS FROM VR.CPP - //============================================================================= - - // NOTE: OpenComposite Compatibility - // The functions below provide compatibility with OpenComposite, which has issues - // with GetDeviceToAbsoluteTrackingPose when requesting poses. We completely avoid - // using GetDeviceToAbsoluteTrackingPose and instead use VRCompositor interfaces - // obtained through BSOpenVR to avoid static linking issues on non-VR systems. - - OpenVRContext::OpenVRContext() - { - openvr = RE::BSOpenVR::GetSingleton(); - if (openvr) { - system = openvr->vrSystem; - overlay = RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext); - } - } - - vr::TrackedDeviceIndex_t GetControllerIndexForDevice(InputDeviceType device, bool isLeftHanded) - { - OpenVRContext ctx; - if (!ctx.IsValid()) - return vr::k_unTrackedDeviceIndexInvalid; - - // Determine the OpenVR role based on handedness and our device enum - vr::ETrackedControllerRole targetRole; - - if (device == InputDeviceType::Primary) { - // Primary controller = dominant hand - targetRole = isLeftHanded ? vr::ETrackedControllerRole::TrackedControllerRole_LeftHand : vr::ETrackedControllerRole::TrackedControllerRole_RightHand; - } else { - // Secondary controller = non-dominant hand - targetRole = isLeftHanded ? vr::ETrackedControllerRole::TrackedControllerRole_RightHand : vr::ETrackedControllerRole::TrackedControllerRole_LeftHand; - } - - // Find controller with the target role - for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - if (ctx.system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { - if (ctx.system->GetControllerRoleForTrackedDeviceIndex(i) == targetRole) { - return i; - } - } - } - return vr::k_unTrackedDeviceIndexInvalid; - } - - bool GetControllerWorldMatrix(vr::TrackedDeviceIndex_t index, float out[3][4]) - { - OpenVRContext ctx; - if (!ctx.IsValid()) - return false; - - vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; - if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) - return false; - - if (!poses[index].bPoseIsValid) - return false; - - for (int i = 0; i < 3; ++i) - for (int j = 0; j < 4; ++j) - out[i][j] = poses[index].mDeviceToAbsoluteTracking.m[i][j]; - return true; - } - - bool GetDeviceToAbsoluteTrackingPoseCompatible(vr::ETrackingUniverseOrigin eOrigin, float fPredictedSecondsToPhotonsFromNow, vr::TrackedDevicePose_t* pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount) - { - (void)fPredictedSecondsToPhotonsFromNow; - (void)eOrigin; - OpenVRContext ctx; - if (!ctx.IsValid()) - return false; - - // For single device requests (common with HMD pose requests), - // use a full pose array to ensure OpenComposite compatibility - if (unTrackedDevicePoseArrayCount == 1) { - vr::TrackedDevicePose_t allPoses[vr::k_unMaxTrackedDeviceCount]; - - // Try to use compositor interface first for better OpenComposite compatibility - // Use BSOpenVR's method to avoid static linking issues - auto* compositor = RE::BSOpenVR::GetIVRCompositor(); - if (!compositor && ctx.openvr) { - // Fallback to compositor from the context - compositor = ctx.openvr->vrContext.vrCompositor; - } - - if (compositor) { - // For OpenComposite compatibility, try to use GetLastPoses which is more stable - auto error = compositor->GetLastPoses(allPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - if (error == vr::VRCompositorError_None) { - // Copy HMD pose (index 0) to output - pTrackedDevicePoseArray[0] = allPoses[0]; - return true; - } - // Fallback to WaitGetPoses if GetLastPoses fails - error = compositor->WaitGetPoses(allPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - if (error == vr::VRCompositorError_None) { - // Copy HMD pose (index 0) to output - pTrackedDevicePoseArray[0] = allPoses[0]; - return true; - } - } - - // If compositor methods failed, return false rather than using the problematic direct call - return false; - } - - // For full device array requests, try compositor first - // Use BSOpenVR's method to avoid static linking issues - auto* compositor = RE::BSOpenVR::GetIVRCompositor(); - if (!compositor && ctx.openvr) { - // Fallback to compositor from the context - compositor = ctx.openvr->vrContext.vrCompositor; - } - - if (compositor) { - // For OpenComposite compatibility, try to use GetLastPoses which is more stable - auto error = compositor->GetLastPoses(pTrackedDevicePoseArray, unTrackedDevicePoseArrayCount, nullptr, 0); - if (error == vr::VRCompositorError_None) { - return true; - } - // Fallback to WaitGetPoses if GetLastPoses fails - error = compositor->WaitGetPoses(pTrackedDevicePoseArray, unTrackedDevicePoseArrayCount, nullptr, 0); - if (error == vr::VRCompositorError_None) { - return true; - } - } - - // If compositor methods failed, return false rather than using the problematic direct call - return false; - } - -} \ No newline at end of file diff --git a/src/Utils/VRUtils.h b/src/Utils/VRUtils.h deleted file mode 100644 index 0e52b283d6..0000000000 --- a/src/Utils/VRUtils.h +++ /dev/null @@ -1,248 +0,0 @@ -#pragma once -#include "D3D.h" -#include "Utils/Input.h" -#include -#include -#include // For ImVec4 -#include -#include - -// Forward declarations - actual definitions are in Features/VR.h -using ControllerDevice = InputDeviceType; -using ButtonCombo = InputCombo; - -/** - * @brief VR utility functions and helpers for OpenVR integration - * - * This namespace provides a collection of utility functions for VR development, - * including overlay management, matrix transformations, controller utilities, - * and UI drawing functions for VR-specific elements. - */ -namespace Util -{ - // ----------------------------------------------------------------------------- - // Centralized UI Colors for Util functions - // ----------------------------------------------------------------------------- - namespace Colors - { - constexpr ImVec4 Primary = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow - constexpr ImVec4 Secondary = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); // Blue - constexpr ImVec4 Both = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green - constexpr ImVec4 Default = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White - } - - inline ImVec4 GetControllerPrimaryColor() { return Colors::Primary; } - inline ImVec4 GetControllerSecondaryColor() { return Colors::Secondary; } - inline ImVec4 GetControllerBothColor() { return Colors::Both; } - inline ImVec4 GetControllerDefaultColor() { return Colors::Default; } - - /** - * @brief Draws a button combination in the ImGui interface with color coding - * @param combo Vector of ButtonCombo structures representing the key combination - * @param showControllerLabels Whether to show controller device labels (Primary/Secondary/Both) - * - * This function renders button combinations with color-coded text: - * - Green: Primary controller - * - Blue: Secondary controller - * - Purple: Both controllers - * - * @example - * ```cpp - * std::vector combo = { ButtonCombo::Primary(kTrigger), ButtonCombo::Secondary(kGrip) }; - * Util::DrawButtonCombo(combo, true); - * ``` - */ - void DrawButtonCombo(const std::vector& combo, bool showControllerLabels); - - /** - * @brief Computes a transformation matrix for positioning an overlay relative to the HMD - * @param offsetX Horizontal offset from HMD in meters (positive = right) - * @param offsetY Vertical offset from HMD in meters (positive = up) - * @param offsetZ Depth offset from HMD in meters (positive = away from user) - * @return HMD transformation matrix with applied offsets - * - * This function gets the current HMD pose and applies the specified offsets - * in HMD local space to create a transformation matrix suitable for overlay positioning. - */ - vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ); - - /** - * @brief Common OpenVR system access pattern with validation - * - * This struct provides a standardized way to access OpenVR interfaces - * with proper validation and error handling. It encapsulates the common - * pattern of getting BSOpenVR singleton and extracting the system and overlay interfaces. - */ - struct OpenVRContext - { - RE::BSOpenVR* openvr = nullptr; ///< BSOpenVR singleton instance - vr::IVRSystem* system = nullptr; ///< OpenVR system interface - vr::IVROverlay* overlay = nullptr; ///< OpenVR overlay interface - - /** - * @brief Constructor that initializes all OpenVR interfaces - * - * Automatically retrieves the BSOpenVR singleton and extracts - * the system and overlay interfaces for immediate use. - */ - OpenVRContext(); - - /** - * @brief Check if basic VR system is available - * @return true if both openvr and system interfaces are valid - */ - bool IsValid() const { return openvr && system; } - - /** - * @brief Check if overlay functionality is available - * @return true if all interfaces (including overlay) are valid - */ - bool HasOverlay() const { return IsValid() && overlay; } - }; - - /** - * @brief Get controller index for our ControllerDevice enum - * @param device The controller device type (Primary/Secondary) - * @param isLeftHanded Whether the user is left-handed (affects primary/secondary mapping) - * @return Tracked device index or vr::k_unTrackedDeviceIndexInvalid if not found - * - * This function maps our ControllerDevice enum to actual OpenVR tracked device indices, - * taking into account user handedness for primary/secondary controller assignment. - */ - vr::TrackedDeviceIndex_t GetControllerIndexForDevice(ControllerDevice device, bool isLeftHanded); - - /** - * @brief Get controller world matrix from OpenVR pose - * @param index The tracked device index of the controller - * @param out Output array[3][4] for the transformation matrix - * @return true if the pose was valid and matrix was retrieved successfully - * - * This function retrieves the current world-space transformation matrix - * for a VR controller in a format compatible with OpenVR matrix operations. - */ - bool GetControllerWorldMatrix(vr::TrackedDeviceIndex_t index, float out[3][4]); - - /** - * @brief OpenComposite-compatible function to get device poses - * @param eOrigin The tracking universe origin - * @param fPredictedSecondsToPhotonsFromNow Prediction time for poses - * @param pTrackedDevicePoseArray Output array for tracked device poses - * @param unTrackedDevicePoseArrayCount Number of poses to retrieve - * @return true if poses were retrieved successfully - * - * This function provides a compatibility layer for getting device poses that works - * with both standard OpenVR and OpenComposite. It uses the compositor interface - * when available for better OpenComposite compatibility. - */ - bool GetDeviceToAbsoluteTrackingPoseCompatible(vr::ETrackingUniverseOrigin eOrigin, float fPredictedSecondsToPhotonsFromNow, vr::TrackedDevicePose_t* pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount); - - //============================================================================= - // MATRIX CONVERSION UTILITIES - //============================================================================= - - /** - * @brief Converts an OpenVR HmdMatrix34_t to a DirectX SimpleMath Matrix - * @param m The OpenVR 3x4 transformation matrix to convert - * @return DirectX SimpleMath 4x4 Matrix with bottom row set to [0,0,0,1] - * - * This function converts between OpenVR's 3x4 transformation matrix format - * and DirectX SimpleMath's 4x4 matrix format, adding the implicit bottom row. - */ - inline Matrix HmdMatrix34ToMatrix(const vr::HmdMatrix34_t& m) - { - // OpenVR matrices are row-major but designed for column-vector math (M * v). - // DirectX SimpleMath uses row-vector math (v * M). - // We need to transpose the rotation and move translation to the bottom row. - return Matrix( - m.m[0][0], m.m[1][0], m.m[2][0], 0.0f, - m.m[0][1], m.m[1][1], m.m[2][1], 0.0f, - m.m[0][2], m.m[1][2], m.m[2][2], 0.0f, - m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); - } - - /** - * @brief Converts a DirectX SimpleMath Matrix to an OpenVR HmdMatrix34_t - * @param mat The DirectX SimpleMath 4x4 matrix to convert - * @return OpenVR 3x4 transformation matrix (bottom row is discarded) - * - * This function converts from DirectX SimpleMath's 4x4 matrix format - * to OpenVR's 3x4 transformation matrix format, discarding the bottom row. - */ - inline vr::HmdMatrix34_t MatrixToHmdMatrix34(const Matrix& mat) - { - vr::HmdMatrix34_t m{}; - // Transpose rotation back (row-vector → column-vector) and extract translation from row 4 - m.m[0][0] = mat._11; - m.m[0][1] = mat._21; - m.m[0][2] = mat._31; - m.m[0][3] = mat._41; - m.m[1][0] = mat._12; - m.m[1][1] = mat._22; - m.m[1][2] = mat._32; - m.m[1][3] = mat._42; - m.m[2][0] = mat._13; - m.m[2][1] = mat._23; - m.m[2][2] = mat._33; - m.m[2][3] = mat._43; - return m; - } - - /** - * @brief Converts a raw 3x4 float array to an OpenVR HmdMatrix34_t - * @param m Raw 3x4 float array in [row][column] format - * @return OpenVR HmdMatrix34_t structure - * - * This function provides a convenient way to convert raw transformation - * matrices from other APIs into OpenVR's matrix format. - */ - inline vr::HmdMatrix34_t Float3x4ToHmdMatrix34(const float m[3][4]) - { - vr::HmdMatrix34_t mat; - for (int i = 0; i < 3; ++i) - for (int j = 0; j < 4; ++j) - mat.m[i][j] = m[i][j]; - return mat; - } - - /** - * @brief Gets the Inter-Pupillary Distance (IPD) from the HMD - * @return IPD in meters, or 0.064 (average human IPD) as fallback - * - * Tries multiple methods to determine IPD: - * 1. Query Prop_UserIpdMeters_Float property directly - * 2. Calculate from eye-to-head transforms - * 3. Fallback to average human IPD (64mm) - */ - inline float GetIPDFromHMD() - { - RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); - if (!openvr || !openvr->vrSystem) - return 0.064f; // Default fallback IPD in meters - - // Method 1: Query IPD property directly - vr::ETrackedPropertyError error = vr::TrackedProp_UnknownProperty; - float ipd = openvr->vrSystem->GetFloatTrackedDeviceProperty( - vr::k_unTrackedDeviceIndex_Hmd, - vr::Prop_UserIpdMeters_Float, - &error); - - if (error == vr::TrackedProp_Success && ipd > 0.0f && ipd < 0.1f) { - return ipd; - } - - // Method 2: Calculate from eye-to-head transforms - vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); - vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); - - // Eye separation is in the X translation component (m[0][3]) - float eyeSeparation = std::abs(leftEye.m[0][3] - rightEye.m[0][3]); - - if (eyeSeparation > 0.0f && eyeSeparation < 0.1f) { - return eyeSeparation; - } - - // Fallback to average human IPD - return 0.064f; - } - -} \ No newline at end of file diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index a17f7e0751..5555268526 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -146,10 +146,6 @@ bool Load() return true; } - if (REL::Module::IsVR()) { - REL::IDDatabase::get().IsVRAddressLibraryAtLeastVersion("0.207.0", true); - } - auto privateProfileRedirectorVersion = Util::GetDllVersion(L"Data/SKSE/Plugins/PrivateProfileRedirector.dll"); if (privateProfileRedirectorVersion.has_value() && privateProfileRedirectorVersion.value().compare(REL::Version(0, 6, 2)) == std::strong_ordering::less) { stl::report_and_fail("Old version of PrivateProfileRedirector detected, 0.6.2+ required if using it."sv); @@ -203,15 +199,8 @@ bool Load() errors.push_back(errorMessage); }; - // Engine Fixes: VR accepts either EngineFixesVR.dll or the EngineFixes.dll NG - if (REL::Module::IsVR()) { - if (!LoadLibrary(L"Data/SKSE/Plugins/EngineFixesVR.dll") && !LoadLibrary(L"Data/SKSE/Plugins/EngineFixes.dll")) { - pushMissingDllError("EngineFixesVR.dll or EngineFixes.dll"); - } - } else { - if (!LoadLibrary(L"Data/SKSE/Plugins/EngineFixes.dll")) { - pushMissingDllError(stl::utf16_to_utf8(L"Data/SKSE/Plugins/EngineFixes.dll").value_or(""s)); - } + if (!LoadLibrary(L"Data/SKSE/Plugins/EngineFixes.dll")) { + pushMissingDllError(stl::utf16_to_utf8(L"Data/SKSE/Plugins/EngineFixes.dll").value_or(""s)); } // Empty RequiredDLLs array, if necessary we can add a dll here in the future without needing to modify the plugin loading logic. diff --git a/tools/verify-shader-refactor.ps1 b/tools/verify-shader-refactor.ps1 index c18954f977..5d5613cf44 100644 --- a/tools/verify-shader-refactor.ps1 +++ b/tools/verify-shader-refactor.ps1 @@ -17,7 +17,7 @@ (e.g. register reorder) can be eyeballed. A refactor that is Tier-1 IDENTICAL on the swept permutations needs no further proof. - Note: the default sweep (VR x HDR_OUTPUT) is strong evidence, not the full build + Note: the default sweep (HDR_OUTPUT x stage) is strong evidence, not the full build matrix from shader-validation.yaml. Pass -Permutations for exotic define combos. .PARAMETER Shader @@ -31,7 +31,7 @@ .PARAMETER Permutations Optional explicit permutation list; each entry is a space-separated define set, - e.g. -Permutations "PSHADER","PSHADER VR". Overrides the auto sweep. + e.g. -Permutations "PSHADER","PSHADER HDR_OUTPUT". Overrides the auto sweep. .PARAMETER Entry Shader entry point. Default: main. @@ -111,9 +111,7 @@ try { if (-not $Permutations -or $Permutations.Count -eq 0) { $Permutations = @( "$stageDefine", - "$stageDefine VR", - "$stageDefine HDR_OUTPUT", - "$stageDefine VR HDR_OUTPUT" + "$stageDefine HDR_OUTPUT" ) } From a3e13d1dc241ca5a41d0a603ae54fb36e5b1b36c Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:55:20 +0100 Subject: [PATCH 41/55] fix(ci): resolve broken job name in shader validation workflow (#2485) Co-authored-by: Claude Opus 4.6 --- .github/workflows/_shared-build.yaml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/_shared-build.yaml b/.github/workflows/_shared-build.yaml index d7b71c3661..abfee6ecef 100644 --- a/.github/workflows/_shared-build.yaml +++ b/.github/workflows/_shared-build.yaml @@ -124,16 +124,10 @@ jobs: shader-validation: if: inputs.run-shader-validation - name: Validate shader compilation (${{ matrix.config.name }}) + name: Validate shader compilation runs-on: windows-2025 permissions: contents: read - strategy: - matrix: - config: - - name: "Flatrim" - file: ".github/configs/shader-validation.yaml" - fail-fast: false steps: - name: Checkout code uses: actions/checkout@v6 @@ -152,7 +146,7 @@ jobs: if: steps.check-hlsl.outputs.skip != 'true' uses: ./.github/actions/setup-build-environment with: - cache-key-suffix: "validation-${{ matrix.config.name }}${{ inputs.cache-key-suffix }}" + cache-key-suffix: "validation${{ inputs.cache-key-suffix }}" - name: Prepare shaders if: steps.check-hlsl.outputs.skip != 'true' uses: ./.github/actions/prepare-shaders @@ -185,7 +179,7 @@ jobs: } } - - name: Validate shader compilation (${{ matrix.config.name }}) + - name: Validate shader compilation if: steps.check-hlsl.outputs.skip != 'true' shell: bash run: | @@ -193,13 +187,13 @@ jobs: echo "fxc.exe not found - shader validation requires fxc.exe. Set --fxc to a valid path or ensure fxc.exe is in PATH." >&2 exit 1 fi - hlslkit-compile --fxc "${{ steps.find_fxc.outputs.fxc_path }}" --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config ${{ matrix.config.file }} --max-warnings 0 --suppress-warnings X1519 + hlslkit-compile --fxc "${{ steps.find_fxc.outputs.fxc_path }}" --shader-dir build/ALL/aio/Shaders --output-dir build/ShaderCache --config .github/configs/shader-validation.yaml --max-warnings 0 --suppress-warnings X1519 - name: Upload shader validation logs if: failure() && steps.check-hlsl.outputs.skip != 'true' uses: actions/upload-artifact@v7 with: - name: shader-validation-logs-${{ matrix.config.name }} + name: shader-validation-logs path: | build/ShaderCache/new_issues.log build/ShaderCache/*.log From dc7e251cf94f8ac764be429c7ef7d1a44a67caae Mon Sep 17 00:00:00 2001 From: doodlum <15017472+doodlum@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:27:42 +0100 Subject: [PATCH 42/55] fix: fix render target properties (#2484) --- src/Deferred.cpp | 4 ---- src/Hooks.cpp | 55 +++++++++++++++++++++++++++++++++++++----------- src/State.cpp | 4 ++-- src/State.h | 2 +- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/Deferred.cpp b/src/Deferred.cpp index aa680f2aa3..3ae3fd0106 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -110,10 +110,6 @@ void Deferred::SetupResources() SetupRenderTarget(MASKS, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R11G11B10_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); // Masks2 (vertexAO; fp16 to allow blending) SetupRenderTarget(MASKS2, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); - - // TAA Water Buffers - SetupRenderTarget(RE::RENDER_TARGETS::kWATER_1, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); - SetupRenderTarget(RE::RENDER_TARGETS::kWATER_2, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); } { diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 9abd07fc9c..8834769157 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -507,8 +507,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - globals::state->ModifyRenderTarget(a_target, a_properties); - func(This, a_target, a_properties); + auto properties = *a_properties; + globals::state->ModifyRenderTarget(a_target, properties); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -520,8 +521,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); - func(This, a_target, a_properties); + auto properties = *a_properties; + properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -530,8 +532,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - a_properties->format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); - func(This, a_target, a_properties); + auto properties = *a_properties; + properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -545,8 +548,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - globals::state->ModifyRenderTarget(a_target, a_properties); - func(This, a_target, a_properties); + auto properties = *a_properties; + globals::state->ModifyRenderTarget(a_target, properties); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -555,8 +559,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - globals::state->ModifyRenderTarget(a_target, a_properties); - func(This, a_target, a_properties); + auto properties = *a_properties; + globals::state->ModifyRenderTarget(a_target, properties); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -565,8 +570,9 @@ namespace Hooks { static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) { - globals::state->ModifyRenderTarget(a_target, a_properties); - func(This, a_target, a_properties); + auto properties = *a_properties; + globals::state->ModifyRenderTarget(a_target, properties); + func(This, a_target, &properties); } static inline REL::Relocation func; }; @@ -593,6 +599,28 @@ namespace Hooks static inline REL::Relocation func; }; + struct CreateRenderTarget_Water1 + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + auto properties = *a_properties; + properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, &properties); + } + static inline REL::Relocation func; + }; + + struct CreateRenderTarget_Water2 + { + static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) + { + auto properties = *a_properties; + properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); + func(This, a_target, &properties); + } + static inline REL::Relocation func; + }; + struct BSShader__BeginTechnique_SetVertexShader { static void thunk(RE::BSGraphics::Renderer*, RE::BSGraphics::VertexShader* a_vertexShader) @@ -893,6 +921,9 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF4F, 0xF51)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF65, 0xF67)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59)); diff --git a/src/State.cpp b/src/State.cpp index 9e43113329..fbc6d07892 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -692,9 +692,9 @@ bool State::IsDeveloperMode() return GetLogLevel() <= spdlog::level::debug; } -void State::ModifyRenderTarget(RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) +void State::ModifyRenderTarget(RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties& a_properties) { - a_properties->supportUnorderedAccess = true; + a_properties.supportUnorderedAccess = true; logger::debug("Adding UAV access to {}", magic_enum::enum_name(a_target)); } diff --git a/src/State.h b/src/State.h index bf42bd6039..4d2f1b156d 100644 --- a/src/State.h +++ b/src/State.h @@ -129,7 +129,7 @@ class State */ bool IsDeveloperMode(); - void ModifyRenderTarget(RE::RENDER_TARGETS::RENDER_TARGET a_targetIndex, RE::BSGraphics::RenderTargetProperties* a_properties); + void ModifyRenderTarget(RE::RENDER_TARGETS::RENDER_TARGET a_targetIndex, RE::BSGraphics::RenderTargetProperties& a_properties); void SetupResources(); From b74dedf962f137e72c9b146e41a1920a640fcc6a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 7 Jun 2026 20:28:52 +0000 Subject: [PATCH 43/55] chore(release): 1.7.0-rc.1 [skip ci] --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index da26c79aae..2034d2bca0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_POLICY_WARNING_CMP0116 OFF) project( # gersemi: ignore CommunityShaders - VERSION 1.6.0 + VERSION 1.7.0 LANGUAGES CXX ) From 47821fa6a87caf9553800e7d513b2b08f23a5863 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 16:03:41 -0700 Subject: [PATCH 44/55] =?UTF-8?q?=EF=BB=BFRevert=20"chore:=20remove=20all?= =?UTF-8?q?=20VR=20support=20(#2475)"=20(restore=20VR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts upstream b48bf2409 within the v1.7.0 sync. The plain merge let #2475's VR deletions win silently on ~148 files our dev left unchanged; this revert restores VR everywhere it was stripped. Conflicts with our newer VR (foveated SSR #82, isVR refactor #113, stereo-opts, restored VRStereoOptimizations and Features/VR/*) were resolved keep-ours -- the merge commit already holds the correct, current VR state for those files. Co-Authored-By: Claude Opus 4.8 (1M context) --- .coderabbit.yaml | 2 +- .github/configs/README.md | 12 +- .github/configs/generate-shader-configs.ps1 | 26 +- .github/copilot-instructions.md | 8 +- AI-INSTRUCTIONS.md | 8 +- CMakeUserPresets.json.template | 2 +- docs/new-feature-template/NewFeature.h | 1 + docs/new-feature-template/NewFeatureReadme.md | 10 +- .../DynamicCubemaps/UpdateCubemapCS.hlsl | 6 +- .../ExponentialHeightFog.hlsli | 46 +- .../VolumetricFogCSCommon.hlsli | 11 +- .../VolumetricFogConservativeDepthCS.hlsl | 10 +- .../VolumetricFogIntegrationCS.hlsl | 6 +- .../VolumetricFogLightScatteringCS.hlsl | 58 +- .../VolumetricFogMaterialCS.hlsl | 5 +- .../ExtendedMaterials/ExtendedMaterials.hlsli | 25 +- .../GrassCollision/GrassCollision.hlsli | 4 +- .../Hair Specular/Shaders/Hair/Hair.hlsli | 10 +- .../LightLimitFix/ClusterBuildingCS.hlsl | 9 +- .../LightLimitFix/ClusterCullingCS.hlsl | 10 +- .../Shaders/LightLimitFix/Common.hlsli | 2 +- .../Shaders/ScreenSpaceGI/blur.cs.hlsl | 26 +- .../Shaders/ScreenSpaceGI/common.hlsli | 13 +- .../Shaders/ScreenSpaceGI/gi.cs.hlsl | 36 +- .../ScreenSpaceGI/prefilterDepths.cs.hlsl | 4 + .../ScreenSpaceGI/radianceDisocc.cs.hlsl | 30 +- .../Shaders/ScreenSpaceGI/stereoSync.cs.hlsl | 122 ++++ .../ScreenSpaceShadows/RaymarchCS.hlsl | 5 + .../ScreenSpaceShadows.hlsli | 2 +- .../ScreenSpaceShadows/StereoSyncCS.hlsl | 169 +++++ .../ScreenSpaceShadows/bend_sss_gpu.hlsli | 40 ++ .../Shaders/SubsurfaceScattering/Burley.hlsli | 6 +- .../SubsurfaceScattering/SeparableSSSCS.hlsl | 3 +- .../Shaders/Upscaling/ClearHMDMaskCS.hlsl | 23 + .../Upscaling/DepthRefractionUpscalePS.hlsl | 28 +- .../Shaders/Upscaling/EncodeTexturesCS.hlsl | 33 +- .../Upscaling/UnderwaterMaskUpscalePS.hlsl | 92 +++ features/VR/CORE | 0 .../VolumetricShadows/VolumetricShadows.hlsli | 10 +- .../Shaders/WaterEffects/WaterCaustics.hlsli | 4 +- .../CommunityShaders/Translations/en.json | 41 +- .../CommunityShaders/Translations/zh_CN.json | 39 +- package/Shaders/Common/FrameBuffer.hlsli | 96 ++- package/Shaders/Common/MotionBlur.hlsli | 6 +- package/Shaders/Common/ShadowSampling.hlsli | 8 +- package/Shaders/Common/SharedData.hlsli | 27 +- package/Shaders/Common/VR.hlsli | 668 ++++++++++++++++++ package/Shaders/DeferredCompositeCS.hlsl | 38 +- package/Shaders/DistantTree.hlsl | 74 +- package/Shaders/Effect.hlsl | 139 +++- .../Shaders/ISApplyVolumetricLighting.hlsl | 31 +- package/Shaders/ISFullScreenVR.hlsl | 69 ++ package/Shaders/ISReflectionsRayTracing.hlsl | 45 +- package/Shaders/ISSAOComposite.hlsl | 17 +- package/Shaders/ISSAOMinify.hlsl | 2 +- package/Shaders/ISTemporalAA.hlsl | 1 + .../ISVolumetricLightingGenerateCS.hlsl | 43 +- package/Shaders/ISWaterBlend.hlsl | 8 +- package/Shaders/Lighting.hlsl | 233 ++++-- package/Shaders/Particle.hlsl | 56 +- package/Shaders/RunGrass.hlsl | 142 +++- package/Shaders/Sky.hlsl | 64 +- package/Shaders/Tests/TestVR.hlsl | 362 ++++++++++ package/Shaders/Tests/TestVRFlat.hlsl | 90 +++ package/Shaders/Utility.hlsl | 114 ++- package/Shaders/VR/InSceneOverlay.ps.hlsl | 18 + package/Shaders/VR/InSceneOverlay.vs.hlsl | 27 + package/Shaders/VR/StereoBlendCS.hlsl | 362 ++++++++++ package/Shaders/VR/VRPostProcessCS.hlsl | 109 +++ .../VRStereoOptimizations/StencilCS.hlsl | 172 +++++ .../VRStereoOptimizations/StencilWritePS.hlsl | 40 ++ .../VRStereoOptimizations/StencilWriteVS.hlsl | 24 + .../VRStereoOptimizations/cbuffers.hlsli | 31 + .../Shaders/VRStereoOptimizations/modes.hlsli | 10 + package/Shaders/Water.hlsl | 201 ++++-- src/CSEditor/Weather/PrecipitationWidget.cpp | 4 +- src/Deferred.cpp | 55 +- src/Deferred.h | 6 +- .../ShadowmapCascadeCullingFix.cpp | 2 +- .../ShadowmapCascadeRasterizerFix.h | 2 +- src/Feature.cpp | 1 + src/Feature.h | 6 + src/FeatureConstraints.h | 2 +- src/FeatureIssues.h | 2 +- src/Features/CSEditor.cpp | 2 +- src/Features/CSEditor.h | 1 + src/Features/CloudShadows.cpp | 2 +- src/Features/CloudShadows.h | 1 + src/Features/DynamicCubemaps.cpp | 2 +- src/Features/DynamicCubemaps.h | 18 +- src/Features/ExponentialHeightFog.cpp | 11 +- src/Features/ExponentialHeightFog.h | 3 +- src/Features/ExtendedMaterials.h | 1 + src/Features/ExtendedTranslucency.h | 1 + src/Features/GrassCollision.cpp | 4 +- src/Features/GrassCollision.h | 3 +- src/Features/GrassLighting.h | 1 + src/Features/HDRDisplay.cpp | 56 +- src/Features/HDRDisplay.h | 1 + src/Features/HairSpecular.h | 1 + src/Features/IBL.h | 1 + src/Features/InteriorSun.cpp | 10 +- src/Features/InteriorSun.h | 1 + src/Features/InverseSquareLighting.h | 1 + src/Features/LODBlending.h | 1 + src/Features/LightLimitFix.cpp | 8 +- src/Features/LightLimitFix.h | 11 +- src/Features/LinearLighting.cpp | 2 +- src/Features/LinearLighting.h | 1 + src/Features/PerformanceOverlay.h | 1 + src/Features/RenderDoc.h | 1 + src/Features/ScreenSpaceGI.cpp | 24 +- src/Features/ScreenSpaceGI.h | 12 +- src/Features/ScreenSpaceShadows.cpp | 166 ++++- src/Features/ScreenSpaceShadows.h | 22 +- src/Features/ScreenshotFeature.h | 3 +- src/Features/Skin.h | 1 + src/Features/SkySync.h | 1 + src/Features/Skylighting.cpp | 22 +- src/Features/Skylighting.h | 7 + src/Features/SubsurfaceScattering.cpp | 2 +- src/Features/SubsurfaceScattering.h | 1 + src/Features/TerrainBlending.cpp | 52 +- src/Features/TerrainBlending.h | 5 +- src/Features/TerrainHelper.cpp | 3 +- src/Features/TerrainHelper.h | 1 + src/Features/TerrainShadows.h | 1 + src/Features/TerrainVariation.h | 1 + src/Features/UnifiedWater.cpp | 2 +- src/Features/UnifiedWater.h | 1 + src/Features/Upscaling.cpp | 274 ++++--- src/Features/Upscaling.h | 46 +- src/Features/Upscaling/DX12SwapChain.cpp | 7 +- src/Features/Upscaling/FidelityFX.cpp | 145 +++- src/Features/Upscaling/RCAS/RCAS.cpp | 4 +- src/Features/Upscaling/Streamline.cpp | 100 ++- src/Features/Upscaling/Streamline.h | 3 +- src/Features/VR.cpp | 2 +- src/Features/VR/InSceneOverlay.cpp | 612 ++++++++++++++++ src/Features/VR/OverlayDrag.cpp | 405 +++++++++++ src/Features/VR/WandPointing.cpp | 146 ++++ src/Features/VolumetricLighting.cpp | 32 +- src/Features/VolumetricLighting.h | 34 + src/Features/VolumetricShadows.cpp | 2 +- src/Features/VolumetricShadows.h | 1 + src/Features/WaterEffects.h | 1 + src/Features/WetnessEffects.h | 1 + src/FrameAnnotations.cpp | 40 +- src/Globals.cpp | 110 ++- src/Globals.h | 3 + src/Hooks.cpp | 35 +- src/Menu.cpp | 31 +- src/Menu.h | 2 + src/Menu/OverlayRenderer.cpp | 17 + src/Menu/OverlayRenderer.h | 5 +- src/Menu/SettingsTabRenderer.cpp | 1 + src/Menu/ThemeManager.cpp | 11 +- src/ShaderCache.cpp | 23 +- src/ShaderCache.h | 35 +- src/State.cpp | 36 +- src/State.h | 3 +- src/TruePBR.h | 1 + src/Utils/D3D.cpp | 8 +- src/Utils/Game.cpp | 8 +- src/Utils/Game.h | 4 +- src/Utils/GameSetting.h | 4 +- src/Utils/Input.h | 97 ++- src/Utils/UI.cpp | 2 + src/Utils/UI.h | 6 +- src/Utils/VRUtils.cpp | 241 +++++++ src/Utils/VRUtils.h | 248 +++++++ 171 files changed, 6886 insertions(+), 777 deletions(-) create mode 100644 features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl create mode 100644 features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl create mode 100644 features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl create mode 100644 features/VR/CORE create mode 100644 package/Shaders/Common/VR.hlsli create mode 100644 package/Shaders/ISFullScreenVR.hlsl create mode 100644 package/Shaders/Tests/TestVR.hlsl create mode 100644 package/Shaders/Tests/TestVRFlat.hlsl create mode 100644 package/Shaders/VR/InSceneOverlay.ps.hlsl create mode 100644 package/Shaders/VR/InSceneOverlay.vs.hlsl create mode 100644 package/Shaders/VR/StereoBlendCS.hlsl create mode 100644 package/Shaders/VR/VRPostProcessCS.hlsl create mode 100644 package/Shaders/VRStereoOptimizations/StencilCS.hlsl create mode 100644 package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl create mode 100644 package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl create mode 100644 package/Shaders/VRStereoOptimizations/cbuffers.hlsli create mode 100644 package/Shaders/VRStereoOptimizations/modes.hlsli create mode 100644 src/Features/VR/InSceneOverlay.cpp create mode 100644 src/Features/VR/OverlayDrag.cpp create mode 100644 src/Features/VR/WandPointing.cpp create mode 100644 src/Utils/VRUtils.cpp create mode 100644 src/Utils/VRUtils.h diff --git a/.coderabbit.yaml b/.coderabbit.yaml index ba8f7d5925..0765e27928 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -12,7 +12,7 @@ reviews: Length: 50 characters limit for title, 72 for body Style: lowercase description, no ending period Examples: - - feat(ssgi): add temporal denoising + - feat(vr): add cross-eye sampling - fix(water): resolve flowmap bug - docs: update shader documentation diff --git a/.github/configs/README.md b/.github/configs/README.md index cd403162c8..3dc2f95584 100644 --- a/.github/configs/README.md +++ b/.github/configs/README.md @@ -5,27 +5,29 @@ This directory contains configuration files used by the CI/CD pipeline for build ## Files - `shader-validation.yaml`: Configuration for shader compilation validation using hlslkit (Skyrim SE) +- `shader-validation-vr.yaml`: VR Configuration for shader compilation validation using hlslkit (Skyrim VR) ## Generating Configuration Files These configuration files can be regenerated using the `generate-shader-configs.ps1` script in this directory. This script requires: -1. A valid Skyrim Special Edition installation +1. A valid Skyrim installation (SE and/or VR) 2. The [hlslkit](https://github.com/alandtse/hlslkit) package installed (`pip install hlslkit`) 3. Community Shaders to be run once with specific settings to generate the required log data ### Prerequisites -Before running the generation script, you must run Skyrim SE **once** with the following Community Shaders settings: +Before running the generation script, you must run each version of Skyrim (SE and VR) **once** with the following Community Shaders settings: 1. **Set Debug Log Level**: In the Community Shaders menu, set the log level to "Debug" or "Trace" 2. **Clear Disk Cache**: Clear the shader disk cache before running 3. **Enable Disk Cache**: Ensure disk cache is enabled and will be saved 4. **Run the Game**: Launch and wait for compilation to complete to generate shader compilation logs -The required log file will be created at: +The required log files will be created at: - **Skyrim SE**: `%USERPROFILE%\Documents\My Games\Skyrim Special Edition\SKSE\CommunityShaders.log` +- **Skyrim VR**: `%USERPROFILE%\Documents\My Games\Skyrim VR\SKSE\CommunityShaders.log` ### Running the Script @@ -50,7 +52,11 @@ The script will: You can also generate the files manually using hlslkit: ```bash +# For Skyrim SE hlslkit-generate --log "%USERPROFILE%\Documents\My Games\Skyrim Special Edition\SKSE\CommunityShaders.log" --output .\.github\configs\shader-validation.yaml + +# For Skyrim VR +hlslkit-generate --log "%USERPROFILE%\Documents\My Games\Skyrim VR\SKSE\CommunityShaders.log" --output .\.github\configs\shader-validation-vr.yaml ``` ## Usage in CI/CD diff --git a/.github/configs/generate-shader-configs.ps1 b/.github/configs/generate-shader-configs.ps1 index f8c78cb408..b93f1924b1 100644 --- a/.github/configs/generate-shader-configs.ps1 +++ b/.github/configs/generate-shader-configs.ps1 @@ -4,9 +4,9 @@ Generates shader validation configuration files for Community Shaders. .DESCRIPTION - This script generates shader-validation.yaml by analyzing Community Shaders log files from - Skyrim Special Edition installations. It requires hlslkit to be installed and Skyrim Special - Edition to have been run with specific settings. + This script generates shader-validation.yaml and shader-validation-vr.yaml files by analyzing + Community Shaders log files from Skyrim installations. It requires hlslkit to be installed + and both Skyrim Special Edition and/or Skyrim VR to have been run with specific settings. .PARAMETER OutputDir Directory where the generated YAML files will be saved. Defaults to current directory. @@ -88,15 +88,33 @@ function Find-SkyrimPaths { } } + # Check for Skyrim VR + $vrPath = Join-Path $myGamesPath "Skyrim VR" + if (Test-Path $vrPath) { + $paths += @{ + Name = "Skyrim VR" + Path = $vrPath + LogPath = Join-Path $vrPath "SKSE\CommunityShaders.log" + ConfigName = "shader-validation-vr.yaml" + Type = "VR" + } + } + # Check CommunityShadersOutputDir environment variable $outputDir = $env:CommunityShadersOutputDir if ($outputDir -and (Test-Path $outputDir)) { Write-Host "Found CommunityShadersOutputDir: $outputDir" -ForegroundColor Yellow + # Try to detect if this is a Skyrim installation by looking for common files $skyrimExe = Get-ChildItem -Path $outputDir -Recurse -Name "SkyrimSE.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + $skyrimVRExe = Get-ChildItem -Path $outputDir -Recurse -Name "SkyrimVR.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($skyrimExe) { Write-Host "Detected Skyrim SE installation in CommunityShadersOutputDir" -ForegroundColor Green } + if ($skyrimVRExe) { + Write-Host "Detected Skyrim VR installation in CommunityShadersOutputDir" -ForegroundColor Green + } } return $paths @@ -189,7 +207,7 @@ if ($LogFile) { $skyrimPaths = Find-SkyrimPaths if ($skyrimPaths.Count -eq 0) { - Write-Error "No Skyrim installations found. Please ensure Skyrim Special Edition is installed." + Write-Error "No Skyrim installations found. Please ensure Skyrim SE or VR is installed." exit 1 } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cc8b8b730f..d8c15c1265 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,7 +13,7 @@ This file provides Copilot-specific guidance while avoiding duplication of the c ## Project Overview -SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/AE. Features runtime shader compilation, 25+ graphics features, and cross-platform Skyrim variant support. +SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/AE/VR. Features runtime shader compilation, 25+ graphics features, and cross-platform Skyrim variant support. ## Environment and Build Essentials @@ -31,7 +31,7 @@ SKSE64 plugin providing modular DirectX 11 graphics enhancements for Skyrim SE/A ### Primary Build Command (Windows) ```powershell -# ALL preset is primary - other presets (SE, AE) are legacy +# ALL preset is primary - other presets (SE, AE, VR) are legacy ./BuildRelease.bat ALL # Universal binary (recommended) ./BuildRelease.bat # Same as ALL (default) ``` @@ -60,7 +60,7 @@ git submodule update --init --recursive # If not cloned with --recursive **Flag potential problems before they occur:** - **Performance Impact**: Graphics features affect rendering performance - suggest user toggles -- **Runtime Compatibility**: Warn about SE/AE compatibility issues, suggest `REL::RelocateMember()` patterns +- **Runtime Compatibility**: Warn about SE/AE/VR compatibility issues, suggest `REL::RelocateMember()` patterns - **Buffer Conflicts**: Highlight GPU register conflicts, recommend hlslkit buffer scanning - **Security Risks**: Validate user input, prevent DirectX crashes from malformed configurations @@ -68,7 +68,7 @@ git submodule update --init --recursive # If not cloned with --recursive - **Complete Solutions**: No TODO/FIXME placeholders - provide fully functional code - **Performance Conscious**: Always consider GPU workload and user experience -- **Cross-Platform**: Ensure changes work across SE/AE variants using runtime detection +- **Cross-Platform**: Ensure changes work across SE/AE/VR variants using runtime detection - **Error Handling**: Include proper resource management and graceful degradation ## Architecture Quick Reference diff --git a/AI-INSTRUCTIONS.md b/AI-INSTRUCTIONS.md index 4bf9d7b42f..3d80045a19 100644 --- a/AI-INSTRUCTIONS.md +++ b/AI-INSTRUCTIONS.md @@ -8,7 +8,7 @@ This file provides guidance for AI assistants working with the Open Shaders code - Build commands and development setup - Architecture overview and critical dependencies (CommonLibSSE-NG) -- Runtime targeting system for SE/AE compatibility +- Runtime targeting system for SE/AE/VR compatibility - Core architecture including Globals system and feature registry - Shader architecture (base shaders in `package/Shaders/`, feature shaders, compute shader patterns) - Development workflows and best practices @@ -18,7 +18,7 @@ This file provides guidance for AI assistants working with the Open Shaders code ### Project Type -SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/AE. +SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/AE/VR. ### Essential Commands @@ -28,7 +28,7 @@ SKSE plugin providing advanced DirectX 11 graphics modifications for Skyrim SE/A ### Build Options -**Runtime Presets**: `ALL` (universal), `SE`, `AE`, `ALL-TRACY` +**Runtime Presets**: `ALL` (universal), `SE`, `AE`, `VR`, `PRE-AE`, `FLATRIM`, `ALL-TRACY` **CMake Options** (set in user preset): @@ -50,7 +50,7 @@ For full details about manual packaging targets (Package-Core, Package-AIO-Manua **Act as an experienced graphics programming and Skyrim modding expert.** -**Key Focus**: Performance impact awareness, runtime compatibility (SE/AE), complete working solutions, DirectX/HLSL best practices. +**Key Focus**: Performance impact awareness, runtime compatibility (SE/AE/VR), complete working solutions, DirectX/HLSL best practices. **Style directives** (see `.claude/CLAUDE.md` "Code Quality Expectations" for full text): diff --git a/CMakeUserPresets.json.template b/CMakeUserPresets.json.template index 86524109e2..3a995a58f3 100644 --- a/CMakeUserPresets.json.template +++ b/CMakeUserPresets.json.template @@ -9,7 +9,7 @@ "DEVBENCH_BRIDGE": "ON" }, "environment": { - "CommunityShadersOutputDir": "F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data" + "CommunityShadersOutputDir": "F:/MySkyrimModpack/mods/CommunityShaders;F:/SteamLibrary/steamapps/common/SkyrimVR/Data;F:/SteamLibrary/steamapps/common/Skyrim Special Edition/Data" }, "inherits": "ALL" } diff --git a/docs/new-feature-template/NewFeature.h b/docs/new-feature-template/NewFeature.h index 86c53cfdf3..1ae1ce3d69 100644 --- a/docs/new-feature-template/NewFeature.h +++ b/docs/new-feature-template/NewFeature.h @@ -38,6 +38,7 @@ struct NewFeature : public Feature } // Functionality + virtual bool inline SupportsVR() override { return true; } virtual inline std::string_view GetShaderDefineName() override { return "SHADER_MACRO"; } virtual inline bool HasShaderDefine(RE::BSShader::Type t) override { return t == RE::BSShader::Type::Lighting; }; diff --git a/docs/new-feature-template/NewFeatureReadme.md b/docs/new-feature-template/NewFeatureReadme.md index d3c0860795..2abe7e6b4d 100644 --- a/docs/new-feature-template/NewFeatureReadme.md +++ b/docs/new-feature-template/NewFeatureReadme.md @@ -60,6 +60,13 @@ YourFeature yourFeature{}; - Customize `DrawSettings()` UI controls - Update shader compilation paths in `CompileShaders()` +### VR Support + +Set `SupportsVR()` return value: + +- `return true;` - Feature works in VR +- `return false;` - Feature disabled in VR builds + ## Naming Conventions | Component | Convention | Example | @@ -79,7 +86,7 @@ The build system automatically handles: - Settings persistence (JSON serialization) - UI menu integration - Feature lifecycle management -- Cross-platform builds (SE/AE) +- Cross-platform builds (SE/AE/VR) Build with: `./BuildRelease.bat ALL` @@ -89,4 +96,5 @@ Build with: `./BuildRelease.bat ALL` - [ ] Settings save/load correctly - [ ] Shaders compile without errors - [ ] Feature works in-game +- [ ] VR compatibility (if enabled) - [ ] No build errors diff --git a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl index b4ef83346f..f6d535d635 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/UpdateCubemapCS.hlsl @@ -1,6 +1,7 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" #include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" RWTexture2DArray DynamicCubemap : register(u0); RWTexture2DArray DynamicCubemapRaw : register(u1); @@ -72,6 +73,7 @@ float smoothbumpstep(float edge0, float edge1, float x) float weight = 0.0; uv = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); + uv = Stereo::ConvertToStereoUV(uv, 0); float depth = DepthTexture.SampleLevel(LinearSampler, uv, 0); float linearDepth = SharedData::GetScreenDepth(depth); @@ -82,7 +84,7 @@ float smoothbumpstep(float edge0, float edge1, float x) if (linearDepth > 16.5 && depth != 1.0) { // Ignore objects which are too close or the sky #endif half4 positionCS = half4(2 * half2(uv.x, -uv.y + 1) - 1, depth, 1); - positionCS = mul(FrameBuffer::CameraViewProjInverse, positionCS); + positionCS = mul(FrameBuffer::CameraViewProjInverse[0], positionCS); positionCS.xyz = positionCS.xyz / positionCS.w; position += positionCS.xyz; @@ -112,7 +114,7 @@ float smoothbumpstep(float edge0, float edge1, float x) } float4 position = DynamicCubemapPosition[ThreadID]; - position.xyz = (position.xyz + (CameraPreviousPosAdjust2.xyz * 0.001)) - (FrameBuffer::CameraPosAdjust.xyz * 0.001); // Remove adjustment, add new adjustment + position.xyz = (position.xyz + (CameraPreviousPosAdjust2.xyz * 0.001)) - (FrameBuffer::CameraPosAdjust[0].xyz * 0.001); // Remove adjustment, add new adjustment DynamicCubemapPosition[ThreadID] = position; float4 color = DynamicCubemapRaw[ThreadID]; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index ede4395433..c15a0280d3 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -23,6 +23,15 @@ namespace ExponentialHeightFog return SharedData::exponentialHeightFogSettings.enabled && SharedData::exponentialHeightFogSettings.disableVanillaFog != 0; } + uint GetEyeIndexFromCameraWS(float3 cameraWS) + { +#if defined(VR) + return distance(cameraWS, FrameBuffer::CameraPosAdjust[1].xyz) < distance(cameraWS, FrameBuffer::CameraPosAdjust[0].xyz) ? 1u : 0u; +#else + return 0u; +#endif + } + bool ShouldApplyVolumetricFog() { return SharedData::exponentialHeightFogSettings.enabled != 0 && @@ -35,9 +44,9 @@ namespace ExponentialHeightFog return max(clipPosition.w, SharedData::CameraData.y); } - float GetSceneDepthForFog(float3 positionWS, out float2 volumeUV, out float projectedDepth) + float GetSceneDepthForFog(float3 positionWS, uint eyeIndex, out float2 volumeUV, out float projectedDepth) { - float4 clipPosition = mul(FrameBuffer::CameraViewProj, float4(positionWS, 1.0f)); + float4 clipPosition = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1.0f)); [branch] if (clipPosition.w <= 0.0f) { volumeUV = 0.0f.xx; @@ -52,7 +61,7 @@ namespace ExponentialHeightFog return projectedDepth; } - float4 SampleVolumetricFog(float3 positionWS) + float4 SampleVolumetricFog(float3 positionWS, uint eyeIndex) { if (!ShouldApplyVolumetricFog()) return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -66,15 +75,25 @@ namespace ExponentialHeightFog float2 volumeUV; float projectedDepth; - float sceneDepth = GetSceneDepthForFog(positionWS, volumeUV, projectedDepth); + float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); if (projectedDepth <= 0.0f) return float4(0.0f, 0.0f, 0.0f, 1.0f); +#if defined(VR) + volumeUV = Stereo::ConvertToStereoUV(volumeUV, eyeIndex); +#endif + float volumeZ = saturate(ComputeVolumetricNormalizedSlice(sceneDepth, float(volumeDepth))); float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); float2 volumeUVMin = volumeTexelCenter.xy; float2 volumeUVMax = 1.0f.xx - volumeTexelCenter.xy; +#if defined(VR) + float eyeMinX = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; + float eyeMaxX = (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x; + volumeUVMin.x = eyeMinX; + volumeUVMax.x = eyeMaxX; +#endif float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); @@ -87,7 +106,7 @@ namespace ExponentialHeightFog return saturate(viewSizeSafe / physicalSize); } - float4 SampleVolumetricFog(float4 screenPosition) + float4 SampleVolumetricFog(float4 screenPosition, uint eyeIndex) { if (!ShouldApplyVolumetricFog()) return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -118,6 +137,10 @@ namespace ExponentialHeightFog float3 volumeTexelCenter = 0.5f / float3(volumeWidth, volumeHeight, volumeDepth); float2 volumeUVMin = volumeTexelCenter.xy; float2 volumeUVMax = max(GetVolumetricFogUVMax(volumeSize, gridPixelSize), volumeUVMin); +#if defined(VR) + volumeUVMin.x = (eyeIndex == 0u ? 0.0f : 0.5f) + volumeTexelCenter.x; + volumeUVMax.x = max(volumeUVMin.x, min(volumeUVMax.x, (eyeIndex == 0u ? 0.5f : 1.0f) - volumeTexelCenter.x)); +#endif float3 volumeUVW = float3(clamp(volumeUV, volumeUVMin, volumeUVMax), clamp(volumeZ, volumeTexelCenter.z, 1.0f - volumeTexelCenter.z)); float4 volumetricFog = ExponentialHeightFogIntegratedLightScattering.SampleLevel(SampColorSampler, volumeUVW, 0); return lerp(float4(0.0f, 0.0f, 0.0f, 1.0f), volumetricFog, saturate((sceneDepth - GetVolumetricStartDistance()) * 100000000.0f)); @@ -149,9 +172,9 @@ namespace ExponentialHeightFog return volumetricFog; } - float4 CombineVolumetricFog(float4 analyticalFog, float3 positionWS, float3 viewDirection) + float4 CombineVolumetricFog(float4 analyticalFog, float3 positionWS, uint eyeIndex, float3 viewDirection) { - float4 volumetricFog = SampleVolumetricFog(positionWS); + float4 volumetricFog = SampleVolumetricFog(positionWS, eyeIndex); volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); float analyticalTransmittance = 1.0f - analyticalFog.w; float combinedTransmittance = volumetricFog.a * analyticalTransmittance; @@ -161,9 +184,9 @@ namespace ExponentialHeightFog return float4(combinedOpacity > 1e-4f ? combinedPremultiplied / combinedOpacity : float3(0.0f, 0.0f, 0.0f), combinedOpacity); } - float4 CombineVolumetricFog(float4 analyticalFog, float4 screenPosition, float3 viewDirection) + float4 CombineVolumetricFog(float4 analyticalFog, float4 screenPosition, uint eyeIndex, float3 viewDirection) { - float4 volumetricFog = SampleVolumetricFog(screenPosition); + float4 volumetricFog = SampleVolumetricFog(screenPosition, eyeIndex); volumetricFog = ApplyDirectionalPhaseCorrection(volumetricFog, viewDirection); float analyticalTransmittance = 1.0f - analyticalFog.w; float combinedTransmittance = volumetricFog.a * analyticalTransmittance; @@ -180,10 +203,11 @@ namespace ExponentialHeightFog if (fogDensity <= 0.0f) { return 0.0f; } + uint eyeIndex = GetEyeIndexFromCameraWS(cameraWS); float3 viewToPos = positionWS; float2 volumeUV; float projectedDepth; - float sceneDepth = GetSceneDepthForFog(positionWS, volumeUV, projectedDepth); + float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); [branch] if (projectedDepth > 1e-4f && sceneDepth > projectedDepth) { viewToPos *= sceneDepth / projectedDepth; @@ -252,7 +276,7 @@ namespace ExponentialHeightFog if (!applyVolumetricFog) { return analyticalFog; } - return useScreenPosition ? CombineVolumetricFog(analyticalFog, screenPosition, viewDirection) : CombineVolumetricFog(analyticalFog, positionWS, viewDirection); + return useScreenPosition ? CombineVolumetricFog(analyticalFog, screenPosition, eyeIndex, viewDirection) : CombineVolumetricFog(analyticalFog, positionWS, eyeIndex, viewDirection); } float4 GetExponentialHeightFog(float3 positionWS, float3 cameraWS, float3 fogColor) diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli index 51aa39331b..bdd69fb861 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogCSCommon.hlsli @@ -2,13 +2,14 @@ #define __EXPONENTIAL_HEIGHT_FOG_VOLUMETRIC_CS_COMMON_HLSLI__ #include "Common/FrameBuffer.hlsli" +#include "Common/VR.hlsli" cbuffer VolumetricFogCB : register(b0) { uint4 VolumetricFogGridSizeAndFlags; float4 VolumetricFogInvGridSizeAndNearFade; float4 VolumetricFogGridZParams; - row_major float4x4 VolumetricFogClipToWorld; + row_major float4x4 VolumetricFogClipToWorld[2]; float4 VolumetricFogFrameJitterOffsets[16]; float4 VolumetricFogHistoryParameters; float4 VolumetricFogJitterParameters; @@ -39,15 +40,17 @@ namespace ExponentialHeightFog return all(coord < VolumetricFogGridSize); } - float3 ComputeCellWorldPosition(uint3 coord, float3 cellOffset, out float viewDepth) + float3 ComputeCellWorldPosition(uint3 coord, float3 cellOffset, out uint eyeIndex, out float viewDepth) { float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; + eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUV); + float2 eyeUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); viewDepth = ComputeVolumetricSliceDepth(max(float(coord.z) + cellOffset.z, 0.0f)); - float2 ndc = volumeUV * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); + float2 ndc = eyeUV * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); float deviceZ = (SharedData::CameraData.x - SharedData::CameraData.w / viewDepth) / SharedData::CameraData.z; - float4 worldPosition = mul(VolumetricFogClipToWorld, float4(ndc, deviceZ, 1.0f)); + float4 worldPosition = mul(VolumetricFogClipToWorld[eyeIndex], float4(ndc, deviceZ, 1.0f)); return worldPosition.xyz / worldPosition.w; } } diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl index f7f8cfad61..7a87efa39e 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogConservativeDepthCS.hlsl @@ -8,12 +8,14 @@ RWTexture2D ConservativeDepthTexture : register(u0); float2 volumeUVMin = (float2(dispatchID.xy) - 0.5f.xx) * VolumetricFogInvGridSize.xy; float2 volumeUVMax = (float2(dispatchID.xy + 1u) + 0.5f.xx) * VolumetricFogInvGridSize.xy; + float2 volumeUVCenter = (float2(dispatchID.xy) + 0.5f.xx) * VolumetricFogInvGridSize.xy; - float2 eyeUVMin = saturate(volumeUVMin); - float2 eyeUVMax = saturate(volumeUVMax); + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(volumeUVCenter); + float2 eyeUVMin = saturate(Stereo::ConvertFromStereoUV(volumeUVMin, eyeIndex)); + float2 eyeUVMax = saturate(Stereo::ConvertFromStereoUV(volumeUVMax, eyeIndex)); - int2 minCoord = SharedData::ConvertUVToSampleCoord(min(eyeUVMin, eyeUVMax)).xy; - int2 maxCoord = SharedData::ConvertUVToSampleCoord(max(eyeUVMin, eyeUVMax)).xy - 1; + int2 minCoord = SharedData::ConvertUVToSampleCoord(min(eyeUVMin, eyeUVMax), eyeIndex).xy; + int2 maxCoord = SharedData::ConvertUVToSampleCoord(max(eyeUVMin, eyeUVMax), eyeIndex).xy - 1; maxCoord = max(maxCoord, minCoord); int2 bufferMax = int2(SharedData::BufferDim.xy) - 1; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl index 16da9896bc..e009d7885b 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogIntegrationCS.hlsl @@ -11,16 +11,18 @@ RWTexture3D IntegratedLightScattering : register(u0); float accumulatedTransmittance = 1.0f; float accumulatedDepth = 0.0f; + uint eyeIndex; float previousDepth; - float3 previousPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(uint3(dispatchID.xy, 0), float3(0.5f, 0.5f, 0.0f), previousDepth); + float3 previousPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(uint3(dispatchID.xy, 0), float3(0.5f, 0.5f, 0.0f), eyeIndex, previousDepth); [loop] for (uint layerIndex = 0; layerIndex < VolumetricFogGridSize.z; layerIndex++) { uint3 layerCoordinate = uint3(dispatchID.xy, layerIndex); float4 scatteringAndExtinction = LightScattering[layerCoordinate]; + uint layerEyeIndex; float layerDepth; - float3 layerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(layerCoordinate, 0.5f.xxx, layerDepth); + float3 layerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(layerCoordinate, 0.5f.xxx, layerEyeIndex, layerDepth); float stepLength = length(layerPositionWS - previousPositionWS); previousPositionWS = layerPositionWS; diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl index 216922ce54..9263c88ba8 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogLightScatteringCS.hlsl @@ -68,10 +68,10 @@ bool IsFroxelBehindSceneDepth(uint3 coord) return sceneDepth < frontDepth; } -float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, out bool validHistory, out float previousViewDepth) +float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, uint eyeIndex, out bool validHistory, out float previousViewDepth) { - float3 previousPositionWS = positionWS + FrameBuffer::CameraPosAdjust.xyz - FrameBuffer::CameraPreviousPosAdjust.xyz; - float4 previousClip = mul(FrameBuffer::CameraPreviousViewProjUnjittered, float4(previousPositionWS, 1.0f)); + float3 previousPositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPreviousPosAdjust[eyeIndex].xyz; + float4 previousClip = mul(FrameBuffer::CameraPreviousViewProjUnjittered[eyeIndex], float4(previousPositionWS, 1.0f)); previousViewDepth = abs(previousClip.w); validHistory = previousClip.w > 0.0f; @@ -79,6 +79,9 @@ float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, out bool validHistory, return 0.0f.xxx; float2 historyUV = previousClip.xy / previousClip.w * float2(0.5f, -0.5f) + 0.5f; +#if defined(VR) + historyUV = Stereo::ConvertToStereoUV(historyUV, eyeIndex); +#endif float historyZ = ExponentialHeightFog::ComputeVolumetricNormalizedSlice(previousViewDepth); float3 volumeUV = float3(historyUV, historyZ); @@ -86,10 +89,10 @@ float3 ComputeHistoryVolumeUVAndDepth(float3 positionWS, out bool validHistory, return saturate(volumeUV); } -float3 ComputeHistoryVolumeUV(float3 positionWS, out bool validHistory) +float3 ComputeHistoryVolumeUV(float3 positionWS, uint eyeIndex, out bool validHistory) { float previousViewDepth; - return ComputeHistoryVolumeUVAndDepth(positionWS, validHistory, previousViewDepth); + return ComputeHistoryVolumeUVAndDepth(positionWS, eyeIndex, validHistory, previousViewDepth); } float2 FixupHistoryUV(float2 uv, float previousCellDepth, out bool validHistory) @@ -156,7 +159,7 @@ float SampleDirectionalShadowPCF(float3 positionLS, uint cascadeIndex) return (center * 4.0f + cross) * rcp(8.0f); } -float SampleDirectionalShadow(float3 positionWS) +float SampleDirectionalShadow(float3 positionWS, uint eyeIndex) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0f; @@ -164,7 +167,7 @@ float SampleDirectionalShadow(float3 positionWS) return 1.0f; DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(positionWS)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(positionWS, eyeIndex)); if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) return 1.0f; @@ -172,7 +175,7 @@ float SampleDirectionalShadow(float3 positionWS) float cascadeSelect = smoothstep(0.0f, 1.0f, saturate((shadowMapDepth - directionalShadowLightData.StartSplitDistances.y) / splitDenom)); uint primaryCascade = (uint)cascadeSelect; - float3 absolutePositionWS = positionWS + FrameBuffer::CameraPosAdjust.xyz; + float3 absolutePositionWS = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; float3 positionLS = mul(directionalShadowLightData.ShadowProj[primaryCascade], float4(absolutePositionWS, 1.0f)).xyz; if (any(positionLS.xy < 0.0f) || any(positionLS.xy > 1.0f)) return 1.0f; @@ -194,14 +197,14 @@ float SampleDirectionalShadow(float3 positionWS) return lerp(1.0f, shadow, fadeFactor); } -float SampleDirectionalWorldShadow(float3 positionWS) +float SampleDirectionalWorldShadow(float3 positionWS, uint eyeIndex) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0f; float worldShadow = 1.0f; #if defined(TERRAIN_SHADOWS) - worldShadow *= TerrainShadows::GetTerrainShadow(positionWS + FrameBuffer::CameraPosAdjust.xyz, LinearSampler); + worldShadow *= TerrainShadows::GetTerrainShadow(positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, LinearSampler); #endif #if defined(CLOUD_SHADOWS) worldShadow *= CloudShadows::GetCloudShadowMult(positionWS, LinearSampler); @@ -209,14 +212,18 @@ float SampleDirectionalWorldShadow(float3 positionWS) return worldShadow; } -float3 ComputeSkyLightScattering(float3 positionWS, float3 viewDirection) +float3 ComputeSkyLightScattering(float3 positionWS, float3 viewDirection, uint eyeIndex) { float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; float3 skyDirection = abs(phaseG) > 0.001f ? normalize(-viewDirection * phaseG) : 0.0f.xxx; float3 skyVisibilityDirection = abs(phaseG) > 0.001f ? skyDirection : float3(0.0f, 0.0f, 1.0f); float skyVisibility = 1.0f; if (VolumetricFogHasSkylighting && !SharedData::InInterior) { +#if defined(VR) + float3 skylightingPosition = positionWS + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +#else float3 skylightingPosition = positionWS; +#endif sh2 skylightingSH = Skylighting::SampleNoBias(skylightingPosition); skyVisibility = Skylighting::EvaluateDiffuse(skylightingSH, skyVisibilityDirection, Skylighting::GetFadeOutFactor(skylightingPosition)); } @@ -251,13 +258,14 @@ float3 AccumulateLocalLightScattering( float3 positionWS, float viewDepth, float3 viewDirection, + uint eyeIndex, float3 materialScattering) { if (!VolumetricFogHasLocalLights) return 0.0f.xxx; float2 volumeUV = (float2(coord.xy) + cellOffset.xy) * VolumetricFogInvGridSize.xy; - float2 screenUV = volumeUV; + float2 screenUV = Stereo::ConvertFromStereoUV(volumeUV, eyeIndex); uint clusterIndex = 0; if (!LightLimitFix::GetClusterIndex(screenUV, viewDepth, clusterIndex)) @@ -266,8 +274,9 @@ float3 AccumulateLocalLightScattering( LightLimitFix::LightGrid grid = LightLimitFix::lightGrid[clusterIndex]; uint lightCount = min(grid.lightCount, (uint)MAX_CLUSTER_LIGHTS); + uint cornerEyeIndex; float cornerViewDepth; - float3 cellCornerWS = ExponentialHeightFog::ComputeCellWorldPosition(coord + uint3(1, 1, 1), cellOffset, cornerViewDepth); + float3 cellCornerWS = ExponentialHeightFog::ComputeCellWorldPosition(coord + uint3(1, 1, 1), cellOffset, cornerEyeIndex, cornerViewDepth); float cellRadius = max(length(cellCornerWS - positionWS), 1.0f); float phaseG = SharedData::exponentialHeightFogSettings.volumetricFogScatteringDistribution; @@ -280,7 +289,7 @@ float3 AccumulateLocalLightScattering( if (light.lightFlags & LightLimitFix::LightFlags::Disabled) continue; - float3 toLight = light.positionWS.xyz - positionWS; + float3 toLight = light.positionWS[eyeIndex].xyz - positionWS; float distanceSqr = dot(toLight, toLight); if (distanceSqr < 1e-6f) continue; @@ -308,6 +317,7 @@ float3 AccumulateLocalLightScattering( float3 positionWS, float viewDepth, float3 viewDirection, + uint eyeIndex, float3 materialScattering) { return 0.0f.xxx; @@ -316,8 +326,9 @@ float3 AccumulateLocalLightScattering( float4 ComputeLightScattering(uint3 coord, float3 cellOffset) { + uint eyeIndex; float viewDepth; - float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(coord, cellOffset, viewDepth); + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(coord, cellOffset, eyeIndex, viewDepth); float4 materialScatteringAndExtinction = VBufferA[coord]; float extinction = materialScatteringAndExtinction.w; @@ -329,8 +340,8 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) // resolution during compositing in SampleVolumetricFog(). float directionalPhase = 1.0f / (4.0f * Math::PI); - float directionalShadow = SampleDirectionalShadow(positionWS) * - SampleDirectionalWorldShadow(positionWS); + float directionalShadow = SampleDirectionalShadow(positionWS, eyeIndex) * + SampleDirectionalWorldShadow(positionWS, eyeIndex); float3 directionalScattering = SharedData::DirLightColor.xyz * SharedData::exponentialHeightFogSettings.volumetricDirectionalScatteringIntensity * @@ -338,7 +349,7 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) directionalPhase * materialScatteringAndExtinction.rgb; - float3 skyScattering = ComputeSkyLightScattering(positionWS, viewDirection) * + float3 skyScattering = ComputeSkyLightScattering(positionWS, viewDirection, eyeIndex) * materialScatteringAndExtinction.rgb; float3 localScattering = AccumulateLocalLightScattering( @@ -347,6 +358,7 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) positionWS, viewDepth, viewDirection, + eyeIndex, materialScatteringAndExtinction.rgb); float3 emissive = SharedData::exponentialHeightFogSettings.volumetricFogEmissive.rgb * @@ -360,21 +372,23 @@ float4 ComputeLightScattering(uint3 coord, float3 cellOffset) if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) return; + uint eyeIndex; float viewDepth; - float3 centerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, viewDepth); + float3 centerPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); if (VolumetricFogHasConservativeDepth && IsFroxelBehindSceneDepth(dispatchID)) { LightScattering[dispatchID] = 0.0f.xxxx; return; } bool validHistory; - float3 historyUV = ComputeHistoryVolumeUV(centerPositionWS, validHistory); + float3 historyUV = ComputeHistoryVolumeUV(centerPositionWS, eyeIndex, validHistory); if (VolumetricFogHasPrevConservativeDepth && validHistory) { + uint frontEyeIndex; float frontDepth; - float3 frontPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, float3(0.5f, 0.5f, -0.5f), frontDepth); + float3 frontPositionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, float3(0.5f, 0.5f, -0.5f), frontEyeIndex, frontDepth); bool validFrontHistory; float previousFrontDepth; - ComputeHistoryVolumeUVAndDepth(frontPositionWS, validFrontHistory, previousFrontDepth); + ComputeHistoryVolumeUVAndDepth(frontPositionWS, frontEyeIndex, validFrontHistory, previousFrontDepth); if (validFrontHistory) { historyUV.xy = saturate(FixupHistoryUV(historyUV.xy, previousFrontDepth, validHistory)); } else { diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl index 7375d1fed5..07687f7015 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/VolumetricFogMaterialCS.hlsl @@ -6,10 +6,11 @@ RWTexture3D VBufferA : register(u0); if (!ExponentialHeightFog::IsInsideVolumetricGrid(dispatchID)) return; + uint eyeIndex; float viewDepth; - float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, viewDepth); + float3 positionWS = ExponentialHeightFog::ComputeCellWorldPosition(dispatchID, 0.5f.xxx, eyeIndex, viewDepth); - float extinction = ExponentialHeightFog::EvaluateHeightFogExtinction(positionWS, FrameBuffer::CameraPosAdjust.xyz); + float extinction = ExponentialHeightFog::EvaluateHeightFogExtinction(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); float3 albedo = saturate(SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.rgb); float3 scattering = extinction * albedo * SharedData::exponentialHeightFogSettings.volumetricFogAlbedo.a; diff --git a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli index 07631d0038..f66859c9da 100644 --- a/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli +++ b/features/Extended Materials/Shaders/ExtendedMaterials/ExtendedMaterials.hlsli @@ -46,6 +46,10 @@ namespace ExtendedMaterials textureDims /= 2.0; #endif +#if defined(VR) + textureDims /= 2.0; +#endif + float2 texCoordsPerSize = coords * textureDims; float2 dxSize = ddx(texCoordsPerSize); @@ -64,6 +68,11 @@ namespace ExtendedMaterials mipLevel++; #endif +// VR: Apply more conservative mipmap level adjustments to reduce over-blurring and shimmering +#if defined(VR) + mipLevel++; +#endif + // Stochastic mip selection: use screen noise to select between adjacent mip levels mipLevel = floor(mipLevel) + (screenNoise < frac(mipLevel) ? 1.0 : 0.0); @@ -312,12 +321,23 @@ namespace ExtendedMaterials StochasticOffsets sharedOffset, float2 dx, float2 dy, # endif out float pixelOffset, +# if defined(VR_STEREO_OPT) + out bool hasPOM, +# endif out float weights[6]) #else - float2 GetParallaxCoords(float distance, float2 coords, float mipLevel, float3 viewDir, float3x3 tbn, float noise, Texture2D tex, SamplerState texSampler, uint channel, DisplacementParams params, out float pixelOffset) + float2 GetParallaxCoords(float distance, float2 coords, float mipLevel, float3 viewDir, float3x3 tbn, float noise, Texture2D tex, SamplerState texSampler, uint channel, DisplacementParams params, out float pixelOffset +# if defined(VR_STEREO_OPT) + , + out bool hasPOM +# endif + ) #endif { pixelOffset = 0.0; +#if defined(VR_STEREO_OPT) + hasPOM = false; +#endif float3 viewDirTS = normalize(mul(tbn, viewDir)); #if defined(LANDSCAPE) viewDirTS.xy /= viewDirTS.z * 0.7 + 0.3 + params[0].FlattenAmount; // Fix for objects at extreme viewing angles @@ -490,6 +510,9 @@ namespace ExtendedMaterials nearBlendToFar *= nearBlendToFar; float offset = (1.0 - parallaxAmount) * -maxHeight + minHeight; pixelOffset = saturate(lerp(parallaxAmount, 0.5, nearBlendToFar)); +#if defined(VR_STEREO_OPT) + hasPOM = true; +#endif return lerp(viewDirTS.xy * offset + coords.xy, coords, nearBlendToFar); } diff --git a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli index c69af8508b..9a751f07cc 100644 --- a/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli +++ b/features/Grass Collision/Shaders/GrassCollision/GrassCollision.hlsli @@ -135,11 +135,11 @@ namespace GrassCollision void GetDisplacedPosition(VS_INPUT input, float3 position, out float3 displacement, out float3 previousDisplacement) { - float3 worldPosition = mul(World, float4(position.xyz, 1.0)).xyz; + float3 worldPosition = mul(World[0], float4(position.xyz, 1.0)).xyz; float nearFactor = smoothstep(2048.0, 0.0, length(worldPosition)); if (input.Color.w > 0.0 && nearFactor > 0.0) { - float3 worldPositionCentre = mul(World, float4(input.InstanceData1.xyz, 1.0)).xyz; + float3 worldPositionCentre = mul(World[0], float4(input.InstanceData1.xyz, 1.0)).xyz; // Limit stretching float3 remappedWorldPosition = lerp(worldPosition, worldPositionCentre, float3(0.95, 0.95, 0.0)); diff --git a/features/Hair Specular/Shaders/Hair/Hair.hlsli b/features/Hair Specular/Shaders/Hair/Hair.hlsli index 15abfbed67..d7d1ea07bb 100644 --- a/features/Hair Specular/Shaders/Hair/Hair.hlsli +++ b/features/Hair Specular/Shaders/Hair/Hair.hlsli @@ -261,7 +261,7 @@ namespace Hair return saturate(lerp(float3(luminance, luminance, luminance), color, saturation)); } - float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise) + float HairSelfShadow(float3 positionWS, float3 lightDirWS, float noise, uint eyeIndex) { if (!SharedData::hairSpecularSettings.EnableSelfShadow) { return 1.0; @@ -270,8 +270,8 @@ namespace Hair // Simple raymarch const int stepCount = 4; - float3 positionVS = FrameBuffer::WorldToView(positionWS); - float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false); + float3 positionVS = FrameBuffer::WorldToView(positionWS, true, eyeIndex); + float3 lightDirVS = FrameBuffer::WorldToView(lightDirWS, false, eyeIndex); lightDirVS *= max(SharedData::hairSpecularSettings.SelfShadowScale * GAME_UNIT_TO_CM, 0.05); float stepSize = 1.0 / stepCount; @@ -282,11 +282,11 @@ namespace Hair [unroll(stepCount)] for (int i = 0; i < stepCount; ++i) { ray += lightDirVS * stepSize; - float2 rayUV = FrameBuffer::ViewToUV(ray); + float2 rayUV = FrameBuffer::ViewToUV(ray, true, eyeIndex); if (FrameBuffer::IsOutsideFrame(rayUV)) continue; float rayDepth = ray.z; - float sampleDepth = SharedData::GetScreenDepth(rayUV); + float sampleDepth = SharedData::GetScreenDepth(rayUV, eyeIndex); if (sampleDepth < rayDepth) { hitCount++; } diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl index da0f1d87b4..8d0134aafa 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterBuildingCS.hlsl @@ -10,14 +10,14 @@ cbuffer PerFrame : register(b0) uint4 ClusterSize; } -float3 GetPositionVS(float2 texcoord, float depth) +float3 GetPositionVS(float2 texcoord, float depth, int eyeIndex = 0) { float4 clipSpaceLocation; clipSpaceLocation.xy = texcoord * 2.0f - 1.0f; // convert from [0,1] to [-1,1] clipSpaceLocation.y *= -1; clipSpaceLocation.z = depth; clipSpaceLocation.w = 1.0f; - float4 homogenousLocation = mul(FrameBuffer::CameraProjInverse, clipSpaceLocation); + float4 homogenousLocation = mul(FrameBuffer::CameraProjInverse[eyeIndex], clipSpaceLocation); return homogenousLocation.xyz / homogenousLocation.w; } @@ -52,8 +52,13 @@ float3 IntersectionZPlane(float3 B, float z_dist) float2 texcoordMax = (groupId.xy + 1) * clusterSize; float2 texcoordMin = groupId.xy * clusterSize; +#if !defined(VR) float3 maxPointVS = GetPositionVS(texcoordMax, 1.0f); float3 minPointVS = GetPositionVS(texcoordMin, 1.0f); +#else + float3 maxPointVS = max(GetPositionVS(texcoordMax, 1.0f, 0), GetPositionVS(texcoordMax, 1.0f, 1)); + float3 minPointVS = min(GetPositionVS(texcoordMin, 1.0f, 0), GetPositionVS(texcoordMin, 1.0f, 1)); +#endif // !VR float clusterNear = LightsNear * pow(abs(LightsFar / LightsNear), groupId.z / float(ClusterSize.z)); float clusterFar = LightsNear * pow(abs(LightsFar / LightsNear), (groupId.z + 1) / float(ClusterSize.z)); diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl index a01e8893f7..2132142131 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl +++ b/features/Light Limit Fix/Shaders/LightLimitFix/ClusterCullingCS.hlsl @@ -49,10 +49,18 @@ bool LightIntersectsCluster(float3 position, float radiusSquared, ClusterAABB cl float radiusSquared = light.radius * light.radius; - float3 positionVS = FrameBuffer::WorldToView(light.positionWS.xyz); +#if defined(VR) + float3 positionVSLeft = FrameBuffer::WorldToView(light.positionWS[0].xyz, true, 0); + float3 positionVSRight = FrameBuffer::WorldToView(light.positionWS[1].xyz, true, 1); + + [branch] if (LightIntersectsCluster(positionVSLeft, radiusSquared, cluster) || LightIntersectsCluster(positionVSRight, radiusSquared, cluster)) + { +#else + float3 positionVS = FrameBuffer::WorldToView(light.positionWS[0].xyz, true, 0); [branch] if (LightIntersectsCluster(positionVS, radiusSquared, cluster)) { +#endif visibleLightIndices[visibleLightCount] = i; visibleLightCount++; if (visibleLightCount >= MAX_CLUSTER_LIGHTS) diff --git a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli index d49acc28ca..ed7bbe3ea7 100644 --- a/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli +++ b/features/Light Limit Fix/Shaders/LightLimitFix/Common.hlsli @@ -44,7 +44,7 @@ struct Light float invRadius; float fadeZone; float sizeBias; - float4 positionWS; + float4 positionWS[2]; uint4 roomFlags; uint lightFlags; uint shadowMapIndex; diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl index 92e37d802c..46e34b175e 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/blur.cs.hlsl @@ -6,6 +6,7 @@ #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" #include "Common/Random.hlsli" +#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcDepth : register(t0); @@ -97,13 +98,14 @@ float2x2 getRotationMatrix(float noise) const uint numSamples = 8; const float2 uv = (dtid + .5) * RCP_OUT_FRAME_DIM; - const float2 screenPos = uv; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + const float2 screenPos = Stereo::ConvertFromStereoUV(uv, eyeIndex); float depth = READ_DEPTH(srcDepth, dtid); - float3 pos = ScreenToViewPosition(screenPos, depth); + float3 pos = ScreenToViewPosition(screenPos, depth, eyeIndex); float3 normal = GBuffer::DecodeNormal(FULLRES_LOAD(srcNormalRoughness, dtid, uv, samplerLinearClamp).xy); - const float2 pixelDirRBViewspaceSizeAtCenterZ = depth.xx * NDCToViewMul.xy * RCP_OUT_FRAME_DIM; + const float2 pixelDirRBViewspaceSizeAtCenterZ = depth.xx * (eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw) * RCP_OUT_FRAME_DIM; const float worldRadius = radius * pixelDirRBViewspaceSizeAtCenterZ.x; float2x3 TvBv = getKernelBasis(normal, normal); // D = N float halfAngle = Math::HALF_PI; @@ -128,18 +130,30 @@ float2x2 getRotationMatrix(float noise) float2 poissonOffset = g_Poisson8[i].xy; + // Project viewspace blur offset to screen. In VR, if the sample leaves the + // current eye, try the other eye for cross-eye consistency at the seam. + // Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" float3 viewSamplePos = pos + TvBv[0] * poissonOffset.x + TvBv[1] * poissonOffset.y; - float2 screenPosSample = FrameBuffer::ViewToUV(viewSamplePos); + float2 screenPosSample = FrameBuffer::ViewToUV(viewSamplePos, true, eyeIndex); + uint sampleEyeIndex = eyeIndex; if (any(screenPosSample < 0) || any(screenPosSample > 1)) { +#ifdef VR + float3 worldSamplePos = FrameBuffer::ViewToWorld(viewSamplePos, true, eyeIndex); + screenPosSample = FrameBuffer::ViewToUV(FrameBuffer::WorldToView(worldSamplePos, true, 1 - eyeIndex), true, 1 - eyeIndex); + sampleEyeIndex = 1 - eyeIndex; + if (any(screenPosSample < 0) || any(screenPosSample > 1)) +#endif continue; } - float2 uvSample = screenPosSample; + float2 uvSample = Stereo::ConvertToStereoUV(screenPosSample, sampleEyeIndex); uvSample = (floor(uvSample * OUT_FRAME_DIM) + 0.5) * RCP_OUT_FRAME_DIM; // Snap to the pixel centre float depthSample = srcDepth.SampleLevel(samplerPointClamp, uvSample * frameScale, RES_MIP); - float3 posSample = ScreenToViewPosition(screenPosSample, depthSample); + float3 posSample = ScreenToViewPosition(screenPosSample, depthSample, sampleEyeIndex); + if (sampleEyeIndex != eyeIndex) + posSample = FrameBuffer::WorldToView(FrameBuffer::ViewToWorld(posSample, true, sampleEyeIndex), true, eyeIndex); float4 normalRoughnessSample = srcNormalRoughness.SampleLevel(samplerPointClamp, uvSample * frameScale, 0); float3 normalSample = GBuffer::DecodeNormal(normalRoughnessSample.xy); diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli b/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli index 86bf0747c8..8abea7831f 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/common.hlsli @@ -25,7 +25,7 @@ cbuffer SSGICB : register(b1) { - float4x4 PrevInvViewMat; + float4x4 PrevInvViewMat[2]; float4 NDCToViewMul; float4 NDCToViewAdd; @@ -86,8 +86,8 @@ float2 filterInf(float2 v) { return float2(filterInf(v.x), filterInf(v.y)); } float3 filterInf(float3 v) { return float3(filterInf(v.x), filterInf(v.y), filterInf(v.z)); } float4 filterInf(float4 v) { return float4(filterInf(v.x), filterInf(v.y), filterInf(v.z), filterInf(v.w)); } -// screenPos - normalised position in FrameDim -// uv - normalised position in FrameDim +// screenPos - normalised position in FrameDim, one eye only +// uv - normalised position in FrameDim, both eye // texCoord - texture coordinate #ifdef HALF_RES @@ -116,10 +116,13 @@ float4 filterInf(float4 v) { return float4(filterInf(v.x), filterInf(v.y), filte /////////////////////////////////////////////////////////////////////////////// // Inputs are screen XY and viewspace depth, output is viewspace position -float3 ScreenToViewPosition(const float2 screenPos, const float viewspaceDepth) +float3 ScreenToViewPosition(const float2 screenPos, const float viewspaceDepth, const uint eyeIndex) { + const float2 _mul = eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw; + const float2 _add = eyeIndex == 0 ? NDCToViewAdd.xy : NDCToViewAdd.zw; + float3 ret; - ret.xy = (NDCToViewMul.xy * screenPos.xy + NDCToViewAdd.xy) * viewspaceDepth; + ret.xy = (_mul * screenPos.xy + _add) * viewspaceDepth; ret.z = viewspaceDepth; return ret; } diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl index 0ad32a8ace..d04b4caa7b 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/gi.cs.hlsl @@ -31,6 +31,7 @@ #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" +#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcWorkingDepth : register(t0); @@ -88,7 +89,8 @@ void CalculateGI( { const float2 frameScale = FrameDim * RcpTexDim; - float2 normalizedScreenPos = uv; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + float2 normalizedScreenPos = Stereo::ConvertFromStereoUV(uv, eyeIndex); const float rcpNumSlices = rcp((float)NumSlices); const float rcpNumSteps = rcp((float)NumSteps); @@ -96,7 +98,7 @@ void CalculateGI( // if the offset is under approx pixel size (pixelTooCloseThreshold), push it out to the minimum distance const float pixelTooCloseThreshold = 1.3; // approx viewspace pixel size at pixCoord; approximation of NDCToViewspace( uv.xy + ViewportSize.xy, pixCenterPos.z ).xy - pixCenterPos.xy; - const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * NDCToViewMul.xy * RCP_OUT_FRAME_DIM; + const float2 pixelDirRBViewspaceSizeAtCenterZ = viewspaceZ.xx * (eyeIndex == 0 ? NDCToViewMul.xy : NDCToViewMul.zw) * RCP_OUT_FRAME_DIM; float screenspaceRadius = EffectRadius / pixelDirRBViewspaceSizeAtCenterZ.x; screenspaceRadius = max(MinScreenRadius, screenspaceRadius); @@ -105,7 +107,8 @@ void CalculateGI( ////////////////////////////////////////////////////////////////// - // Use screen-space position for noise indexing. + // Use mono screen-space position for noise indexing so both eyes + // sample the same noise for corresponding world positions. uint2 noiseCoord = uint2(normalizedScreenPos * OUT_FRAME_DIM); const float2 localNoise = SpatioTemporalNoise(noiseCoord, FrameIndex); const float noiseSlice = localNoise.x; @@ -113,7 +116,7 @@ void CalculateGI( ////////////////////////////////////////////////////////////////// - const float3 pixCenterPos = ScreenToViewPosition(normalizedScreenPos, viewspaceZ); + const float3 pixCenterPos = ScreenToViewPosition(normalizedScreenPos, viewspaceZ, eyeIndex); const float3 viewVec = normalize(-pixCenterPos); #ifdef GI_SPECULAR const float NoV = clamp(dot(viewVec, viewspaceNormal), 1e-5, 1); @@ -183,7 +186,11 @@ void CalculateGI( float2 samplePxCoord = dtid + .5 + sampleOffset * sideSign; float2 sampleUV = samplePxCoord * RCP_OUT_FRAME_DIM; - float2 sampleScreenPos = sampleUV; + // Resolve which eye owns this sample. In VR, radial steps can cross the + // eye boundary in the side-by-side buffer; re-decode with the correct eye. + // Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" + uint sampleEyeIndex = Stereo::GetEyeIndexFromTexCoord(sampleUV); + float2 sampleScreenPos = Stereo::ConvertFromStereoUV(sampleUV, sampleEyeIndex); [branch] if (any(sampleScreenPos > 1.0) || any(sampleScreenPos < 0.0)) continue; // Mip level grows with pixel-space distance from the centre. @@ -202,9 +209,17 @@ void CalculateGI( float SZ = srcWorkingDepth.SampleLevel(samplerPointClamp, sampleUV * frameScale, mipLevel); - // Reconstruct sample viewspace position for correct horizon angles. - float3 samplePos = ScreenToViewPosition(sampleScreenPos, SZ); - // Reject if the depth differs too much from the center pixel. + // Reconstruct sample in current eye's viewspace for correct horizon angles. + float3 samplePos = ScreenToViewPosition(sampleScreenPos, SZ, sampleEyeIndex); + // For cross-eye samples, reject if the depth differs too much from the + // center pixel -- the other eye may see a different surface due to occlusion. +#if defined(VR) + if (sampleEyeIndex != eyeIndex) { + if (abs(SZ - viewspaceZ) > viewspaceZ * 0.1) + continue; + samplePos = FrameBuffer::WorldToView(FrameBuffer::ViewToWorld(samplePos, true, sampleEyeIndex), true, eyeIndex); + } +#endif float3 sampleDelta = samplePos - pixCenterPos; float3 sampleHorizonVec = normalize(sampleDelta); @@ -261,7 +276,7 @@ void CalculateGI( frontBackMult = frontBackMult < 0 ? 0.0 : frontBackMult; // backface if (frontBackMult > 0.f) { - float3 sampleHorizonVecWS = normalize(mul(FrameBuffer::CameraViewInverse, half4(sampleHorizonVec, 0)).xyz); + float3 sampleHorizonVecWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], half4(sampleHorizonVec, 0)).xyz); float3 sampleRadiance = srcRadiance.SampleLevel(samplerPointClamp, sampleUV * OUT_FRAME_SCALE, mipLevelRadiance).rgb * frontBackMult * giBoost * countbits(validBits) * 0.03125; sampleRadiance = max(sampleRadiance, 0); @@ -332,13 +347,14 @@ void CalculateGI( uint2 pxCoord = dtid; float2 uv = (pxCoord + .5) * RCP_OUT_FRAME_DIM; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); float viewspaceZ = READ_DEPTH(srcWorkingDepth, pxCoord); float2 normalSample = FULLRES_LOAD(srcNormal, pxCoord, uv * OUT_FRAME_SCALE, samplerLinearClamp); float3 viewspaceNormal = GBuffer::DecodeNormal(normalSample); - half2 encodedWorldNormal = GBuffer::EncodeNormal(ViewToWorldVector(viewspaceNormal, FrameBuffer::CameraViewInverse)); + half2 encodedWorldNormal = GBuffer::EncodeNormal(ViewToWorldVector(viewspaceNormal, FrameBuffer::CameraViewInverse[eyeIndex])); outPrevGeo[pxCoord] = half3(viewspaceZ, encodedWorldNormal); // Move center pixel slightly towards camera to avoid imprecision artifacts due to depth buffer imprecision; offset depends on depth texture format used diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl index 442ce5a87d..62ae96f1ec 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/prefilterDepths.cs.hlsl @@ -27,6 +27,10 @@ RWTexture2D outDepth4 : register(u4); // is required to be non-linear (i.e. very large outdoors environments). float ClampDepth(float depth) { +#ifdef VR + if (depth == 0.0) // VR 0 indicates a mask + return 0.0; +#endif depth = ScreenToViewDepth(depth); return clamp(depth, 0.0, 3.402823466e+38); } diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl index f243bad154..d0fc087d63 100644 --- a/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/radianceDisocc.cs.hlsl @@ -2,6 +2,7 @@ #include "Common/FrameBuffer.hlsli" #include "Common/GBuffer.hlsli" #include "Common/Math.hlsli" +#include "Common/VR.hlsli" #include "ScreenSpaceGI/common.hlsli" Texture2D srcDiffuse : register(t0); @@ -27,11 +28,11 @@ RWTexture2D outRemappedPrevGISpecular : register(u5); #endif void readHistory( - float curr_depth, float3 curr_pos, int2 pixCoord, float bilinear_weight, + uint eyeIndex, float curr_depth, float3 curr_pos, int2 pixCoord, float bilinear_weight, inout half prev_ao, inout half4 prev_y, inout half2 prev_co_cg, inout half3 prev_ambient, inout float accum_frames, inout half4 prev_gi_specular, inout float wsum) { const float2 uv = (pixCoord + .5) * RCP_OUT_FRAME_DIM; - const float2 screen_pos = uv; + const float2 screen_pos = Stereo::ConvertFromStereoUV(uv, eyeIndex); if (any(screen_pos < 0) || any(screen_pos > 1)) return; @@ -42,12 +43,12 @@ void readHistory( // Early reject: skip bilinear taps on a different surface before the // expensive world-space reconstruction. Use a wider threshold than the // world-space check to avoid rejecting valid taps displaced by parallax - // (e.g. camera rotation). + // (e.g. VR head rotation). if (abs(curr_depth - prev_depth) > curr_depth * DepthDisocclusion * 3) return; - float3 prev_pos = ScreenToViewPosition(screen_pos, prev_depth); - prev_pos = ViewToWorldPosition(prev_pos, PrevInvViewMat) + FrameBuffer::CameraPreviousPosAdjust.xyz; + float3 prev_pos = ScreenToViewPosition(screen_pos, prev_depth, eyeIndex); + prev_pos = ViewToWorldPosition(prev_pos, PrevInvViewMat[eyeIndex]) + FrameBuffer::CameraPreviousPosAdjust[eyeIndex].xyz; float3 delta_pos = curr_pos - prev_pos; // float normal_prod = dot(curr_normal, prev_normal); @@ -74,13 +75,14 @@ void readHistory( const float2 frameScale = FrameDim * RcpTexDim; const float2 uv = (pixCoord + .5) * RCP_OUT_FRAME_DIM; - const float2 screen_pos = uv; + const uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + const float2 screen_pos = Stereo::ConvertFromStereoUV(uv, eyeIndex); float2 prev_screen_pos = screen_pos; #ifdef REPROJECTION prev_screen_pos += FULLRES_LOAD(srcMotionVec, pixCoord, uv * frameScale, samplerLinearClamp).xy; #endif - float2 prev_uv = prev_screen_pos; + float2 prev_uv = Stereo::ConvertToStereoUV(prev_screen_pos, eyeIndex); half3 prev_ambient = 0; half prev_ao = 0; @@ -103,24 +105,24 @@ void readHistory( #ifdef REPROJECTION if ((curr_depth <= DepthFadeRange.y) && !(any(prev_screen_pos < 0) || any(prev_screen_pos > 1))) { // float3 curr_normal = GBuffer::DecodeNormal(srcCurrNormal[pixCoord]); - // curr_normal = ViewToWorldVector(curr_normal, FrameBuffer::CameraViewInverse); - float3 curr_pos = ScreenToViewPosition(screen_pos, curr_depth); - curr_pos = ViewToWorldPosition(curr_pos, FrameBuffer::CameraViewInverse) + FrameBuffer::CameraPosAdjust.xyz; + // curr_normal = ViewToWorldVector(curr_normal, FrameBuffer::CameraViewInverse[eyeIndex]); + float3 curr_pos = ScreenToViewPosition(screen_pos, curr_depth, eyeIndex); + curr_pos = ViewToWorldPosition(curr_pos, FrameBuffer::CameraViewInverse[eyeIndex]) + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; float2 prev_px_coord = prev_uv * OUT_FRAME_DIM; int2 prev_px_lu = floor(prev_px_coord - 0.5); float2 bilinear_weights = prev_px_coord - 0.5 - prev_px_lu; - readHistory(curr_depth, curr_pos, + readHistory(eyeIndex, curr_depth, curr_pos, prev_px_lu, (1 - bilinear_weights.x) * (1 - bilinear_weights.y), prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(curr_depth, curr_pos, + readHistory(eyeIndex, curr_depth, curr_pos, prev_px_lu + int2(1, 0), bilinear_weights.x * (1 - bilinear_weights.y), prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(curr_depth, curr_pos, + readHistory(eyeIndex, curr_depth, curr_pos, prev_px_lu + int2(0, 1), (1 - bilinear_weights.x) * bilinear_weights.y, prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); - readHistory(curr_depth, curr_pos, + readHistory(eyeIndex, curr_depth, curr_pos, prev_px_lu + int2(1, 1), bilinear_weights.x * bilinear_weights.y, prev_ao, prev_y, prev_co_cg, prev_ambient, accum_frames, prev_gi_specular, wsum); diff --git a/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl b/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl new file mode 100644 index 0000000000..365e50236f --- /dev/null +++ b/features/Screen Space GI/Shaders/ScreenSpaceGI/stereoSync.cs.hlsl @@ -0,0 +1,122 @@ +// Stereo Sync - Bilateral blend of SSGI buffers between eyes +// +// Reprojects each pixel to the other eye and blends AO/IL based on depth +// agreement. Runs after the SSGI blur to reduce per-eye GI disparities. +// +// Based on: Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space +// ambient occlusion" https://eprints.whiterose.ac.uk/id/eprint/187713/ + +#include "Common/FrameBuffer.hlsli" +#include "Common/VR.hlsli" +#include "ScreenSpaceGI/common.hlsli" + +#ifdef VR + +Texture2D srcDepth : register(t0); +Texture2D srcAo : register(t1); +Texture2D srcIlY : register(t2); +Texture2D srcIlCoCg : register(t3); + +RWTexture2D outAo : register(u0); +RWTexture2D outIlY : register(u1); +RWTexture2D outIlCoCg : register(u2); + +static const float kDepthSigma = 0.01; // Bilateral depth tolerance (NDC): surfaces within this range are considered the same and blended +static const float kMaxBlend = 0.5; // Maximum stereo blend weight; 0.5 gives equal weighting between eyes +static const float kEdgeRelThreshold = 0.5; // Relative linear-depth difference above which a pixel is a depth discontinuity (50% change) +static const float kMaskDepth = 0.01; // Linear depth sentinel: values below this are outside the HMD lens area +static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check + +// Writes all output channels from the source buffers (passthrough / no-blend path). +void Passthrough(uint2 dtid) +{ + outAo[dtid] = srcAo[dtid]; + outIlY[dtid] = srcIlY[dtid]; + outIlCoCg[dtid] = srcIlCoCg[dtid]; +} + +// Samples four depth neighbors in a cross pattern (±step.x, ±step.y) around centerUV, +// scaled by texScale to map from output UV space to texture sample coords. +// centerUV is clamped to eyeIndex's half of the stereo buffer before offsetting +// to prevent neighbor reads from crossing the x=0.5 seam into the other eye. +float4 SampleCrossDepths(float2 centerUV, float2 step, float2 texScale, uint eyeIndex) +{ + float2 uv = Stereo::ClampToEyeUV(centerUV, eyeIndex); + return float4( + srcDepth.SampleLevel(samplerPointClamp, (uv + float2(step.x, 0)) * texScale, RES_MIP), + srcDepth.SampleLevel(samplerPointClamp, (uv + float2(-step.x, 0)) * texScale, RES_MIP), + srcDepth.SampleLevel(samplerPointClamp, (uv + float2(0, step.y)) * texScale, RES_MIP), + srcDepth.SampleLevel(samplerPointClamp, (uv + float2(0, -step.y)) * texScale, RES_MIP)); +} + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + const float2 outFrameDim = OUT_FRAME_DIM; + if (any(dtid >= uint2(outFrameDim))) + return; + + const float2 frameScale = FrameDim * RcpTexDim; + float2 uv = (dtid + 0.5) / outFrameDim; + + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + // SSGI working depth is linear view-space Z. + // 0.0 = mask (outside lens area). FP_Z = first-person hands threshold (~18.0). + float depth = srcDepth.SampleLevel(samplerPointClamp, uv * frameScale, RES_MIP); + if (depth < FP_Z) { + Passthrough(dtid); + return; + } + + // Source edge detection: skip stereo sync at depth discontinuities. + // Uses a relative threshold since depth is linear view-space (not NDC). + // Placed before rawDepth conversion and reprojection to save VP matrix work + // for edge pixels. + float2 pixelStep = 1.0 / outFrameDim; + float4 srcNeighborDepths = SampleCrossDepths(uv, pixelStep, frameScale, eyeIndex); + if (Stereo::MaxDepthDiff(depth, srcNeighborDepths) / max(depth, 1.0) > kEdgeRelThreshold) { + Passthrough(dtid); + return; + } + + // Convert linear depth to raw depth (NDC Z) for reprojection matrix math. + // raw = (CameraData.x - CameraData.w / depth) / CameraData.z + // where x=n*f, w=f, z=f-n + float rawDepth = (SharedData::CameraData.x - SharedData::CameraData.w / depth) / SharedData::CameraData.z; + + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, rawDepth, eyeIndex, outFrameDim); + + if (!r.valid) { + Passthrough(dtid); + return; + } + + float otherLinearDepth = srcDepth.SampleLevel(samplerPointClamp, r.otherStereoUV * frameScale, RES_MIP); + if (otherLinearDepth < FP_Z) { + Passthrough(dtid); + return; + } + + // Destination edge detection: skip if the reprojected pixel is near the HMD mask + // boundary or at a depth discontinuity in the other eye. Due to VR parallax the + // arm silhouette appears at a different screen position per eye, so the reprojection + // can cross a boundary invisible from this eye's perspective. + float2 marginStep = float(kEdgeMargin) / outFrameDim; + float4 otherNeighborDepths = SampleCrossDepths(r.otherStereoUV, marginStep, frameScale, 1 - eyeIndex); + if (any(otherNeighborDepths < kMaskDepth) || + Stereo::MaxDepthDiff(otherLinearDepth, otherNeighborDepths) / max(otherLinearDepth, 1.0) > kEdgeRelThreshold) { + Passthrough(dtid); + return; + } + + float otherRawDepth = (SharedData::CameraData.x - SharedData::CameraData.w / otherLinearDepth) / SharedData::CameraData.z; + + // Back-check disabled: source + destination edge detection covers the occlusion + // boundary cases it was guarding, saving 2 VP matrix multiplies per blended pixel. + Stereo::FinalizeStereoBlend(r, uv, rawDepth, otherRawDepth, eyeIndex, outFrameDim, kDepthSigma, kMaxBlend, 0.0); + + outAo[dtid] = lerp(srcAo[dtid], srcAo[r.otherPx], r.blendWeight); + outIlY[dtid] = lerp(srcIlY[dtid], srcIlY[r.otherPx], r.blendWeight); + outIlCoCg[dtid] = lerp(srcIlCoCg[dtid], srcIlCoCg[r.otherPx], r.blendWeight); +} + +#endif // VR diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl index 62971b5383..17078de682 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/RaymarchCS.hlsl @@ -56,7 +56,12 @@ cbuffer PerFrame : register(b1) parameters.DynamicRes = DynamicRes; +#if defined(VR) + // Disabled in VR: depth bias causes subtle shadow shifting at stereo seams on camera motion. + parameters.UsePrecisionOffset = false; +#else parameters.UsePrecisionOffset = true; +#endif WriteScreenSpaceShadow(parameters, groupID, groupThreadID); } \ No newline at end of file diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli index 9f94e4c708..a1a6929c33 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/ScreenSpaceShadows.hlsli @@ -3,7 +3,7 @@ namespace ScreenSpaceShadows { Texture2D ScreenSpaceShadowsTexture : register(t45); - float GetScreenSpaceShadow(float3 screenPosition, float2 uv, float noise) + float GetScreenSpaceShadow(float3 screenPosition, float2 uv, float noise, uint eyeIndex) { return ScreenSpaceShadowsTexture.Load(int3(int2(screenPosition.xy + 0.5f), 0)).x; } diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl new file mode 100644 index 0000000000..47e88e9f90 --- /dev/null +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/StereoSyncCS.hlsl @@ -0,0 +1,169 @@ +// Stereo Sync + Blur - Combined bilateral stereo blend and depth-weighted +// blur for VR screen-space shadows. Runs as a single compute pass after the +// raymarch to both synchronize shadow data between eyes and smooth per-pixel +// noise. +// +// Based on: Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space +// ambient occlusion" https://eprints.whiterose.ac.uk/id/eprint/187713/ + +#include "Common/FrameBuffer.hlsli" +#include "Common/Math.hlsli" +#include "Common/Random.hlsli" +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" + +#ifdef VR + +// Match the C++ depth binding format for strict typing. +// TERRAIN_BLENDING ON -> R32_FLOAT (no unorm). OFF -> R24_UNORM_X8_TYPELESS (unorm). +# if defined(TERRAIN_BLENDING) +Texture2D SrcDepthTexture : register(t0); +# else +Texture2D SrcDepthTexture : register(t0); +# endif +Texture2D SrcShadowTexture : register(t1); + +RWTexture2D OutShadowTexture : register(u0); + +cbuffer StereoSyncCB : register(b1) +{ + float2 FrameDim; + float2 RcpFrameDim; +}; + +static const float kDepthSigma = 0.01; // Bilateral depth tolerance (NDC): surfaces within this range are considered the same and blended +static const float kMaxBlend = 1.0; // Maximum stereo blend weight; reduce below 1.0 to soften the cross-eye contribution +static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo sync +static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check + +// Depth-weighted 4-sample blur using a rotated Poisson disk. +// Uses dtid hash for per-pixel rotation to break structured patterns. +float BlurShadow(int2 dtid, float centerDepth) +{ + // Per-pixel rotation from interleaved gradient noise + float noise = Random::InterleavedGradientNoise(float2(dtid)); + float angle = noise * Math::TAU; + float sn, cs; + sincos(angle, sn, cs); + float2x2 rot = float2x2(cs, sn, -sn, cs); + + static const float2 kOffsets[4] = { + float2(0.382, 0.892), + float2(0.491, 0.217), + float2(0.938, 0.735), + float2(0.009, 0.056), + }; + + float weight = 0; + float shadow = 0; + + [unroll] for (uint i = 0; i < 4; i++) + { + float2 offset = mul(kOffsets[i], rot); + int2 samplePx = dtid + int2(offset * 2.5); + samplePx = clamp(samplePx, int2(0, 0), int2(FrameDim) - 1); + + float sampleDepth = SrcDepthTexture[samplePx]; + + if (sampleDepth < 1e-5) + continue; + + float attenuation = 1.0 - saturate(100.0 * abs(sampleDepth - centerDepth) / max(centerDepth, 1e-5)); + + if (attenuation > 0.0) { + shadow += SrcShadowTexture[samplePx] * attenuation; + weight += attenuation; + } + } + + return weight > 0.0 ? shadow / weight : SrcShadowTexture[dtid]; +} + +// Samples four depth neighbors in a cross pattern (±offset pixels) around center, +// clamped to eyeIndex's half of the packed stereo buffer to avoid seam contamination. +float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) +{ + return float4( + SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(offset, 0), eyeIndex, FrameDim)], + SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(-offset, 0), eyeIndex, FrameDim)], + SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(0, offset), eyeIndex, FrameDim)], + SrcDepthTexture[Stereo::ClampToEyeBounds(center + int2(0, -offset), eyeIndex, FrameDim)]); +} + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + + float2 uv = (dtid + 0.5) * RcpFrameDim; + + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float depth = SrcDepthTexture[dtid]; + + // depth == 0: VR HMD mask; depth == 1: sky/far plane + if (depth < 1e-5 || depth >= 1.0) { + OutShadowTexture[dtid] = SrcShadowTexture[dtid]; + return; + } + + // Skip stereo sync for first-person geometry interior (hands/weapons). + // Placed before the blur: arm shadow is uniform so the bilateral blur + // would return SrcShadowTexture[dtid] unchanged anyway. + float linearDepth = SharedData::GetScreenDepth(depth); + if (linearDepth < VR_FP_Z) { + OutShadowTexture[dtid] = SrcShadowTexture[dtid]; + return; + } + + // Skip stereo sync at depth discontinuities (arm/world silhouettes, object edges). + // Placed before the blur: the bilateral depth weighting zeroes out cross-edge + // samples, so the blur collapses to SrcShadowTexture[dtid] at these pixels anyway. + float4 edgeDepths = SampleCrossDepths(dtid, 1, eyeIndex); + if (Stereo::MaxDepthDiff(depth, edgeDepths) > kEdgeDepthThreshold) { + OutShadowTexture[dtid] = SrcShadowTexture[dtid]; + return; + } + + // Depth-weighted blur on this eye's shadow data. + // Only reached by world pixels that will attempt stereo sync. + float myShadow = BlurShadow(dtid, depth); + + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, depth, eyeIndex, FrameDim); + + if (!r.valid) { + OutShadowTexture[dtid] = myShadow; + return; + } + + float otherDepth = SrcDepthTexture[r.otherPx]; + + // Skip if other eye sees mask, sky, or first-person geometry + if (otherDepth < 1e-5 || otherDepth >= 1.0 || SharedData::GetScreenDepth(otherDepth) < VR_FP_Z) { + OutShadowTexture[dtid] = myShadow; + return; + } + + // Reject if reprojected pixel is near the HMD mask boundary, or if it sits + // at a depth discontinuity in the other eye. The source-side edge check above + // only fires when *this* eye sees the boundary; due to VR parallax the arm + // silhouette appears at a different screen position in each eye, so the + // reprojection can cross a boundary invisible from this eye's perspective. + // Reusing the same four neighbor reads covers both purposes at no extra cost. + float4 otherNeighbors = SampleCrossDepths(r.otherPx, kEdgeMargin, 1 - eyeIndex); + if (any(otherNeighbors < 1e-5) || Stereo::MaxDepthDiff(otherDepth, otherNeighbors) > kEdgeDepthThreshold) { + OutShadowTexture[dtid] = myShadow; + return; + } + + // Source + destination edge detection + Stereo::FinalizeStereoBlend(r, uv, depth, otherDepth, eyeIndex, FrameDim, kDepthSigma, kMaxBlend, 0.0); + + float otherShadow = SrcShadowTexture[r.otherPx]; + + // Use min (darkest) when depths agree: if either eye detected an + // occluder, that shadow should be visible. + float combined = min(myShadow, otherShadow); + OutShadowTexture[dtid] = lerp(myShadow, combined, r.blendWeight); +} + +#endif // VR diff --git a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli index 5ca905514a..832f578fc3 100644 --- a/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli +++ b/features/Screen-Space Shadows/Shaders/ScreenSpaceShadows/bend_sss_gpu.hlsli @@ -253,8 +253,35 @@ void WriteScreenSpaceShadow(DispatchParameters inParameters, int3 inGroupID, int half2 coord = read_xy * inParameters.InvDepthTextureSize * inParameters.DynamicRes; half2 coord_with_offset = (read_xy + offset_xy) * inParameters.InvDepthTextureSize * inParameters.DynamicRes; +#if defined(VR) + // VR side-by-side: halve x to map stereo pixel coords to texture UV. + coord *= half2(0.5, 1.0); + coord_with_offset *= half2(0.5, 1.0); + +# if defined(RIGHT) + // Right eye: valid UV range is [0.5*DynRes.x, DynRes.x] + bool coord_out_of_eye = coord.x < 0.5 * inParameters.DynamicRes.x; + bool coord_offset_out_of_eye = coord_with_offset.x < 0.5 * inParameters.DynamicRes.x; +# else + // Left eye: valid UV range is [0.0, 0.5*DynRes.x) + bool coord_out_of_eye = coord.x >= 0.5 * inParameters.DynamicRes.x; + bool coord_offset_out_of_eye = coord_with_offset.x >= 0.5 * inParameters.DynamicRes.x; +# endif + + // Clamp cross-eye depth reads to FarDepthValue (1.0) so rays near the SBS center + // seam see no occluder at the boundary. Shadow weakens by ~1 pixel at the seam but + // stays temporally stable across camera movement. + depths.x = coord_out_of_eye ? 1.0 : inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord, 0); + depths.y = coord_offset_out_of_eye ? 1.0 : inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord_with_offset, 0); + + // HMD mask: depth==0 is outside the visible lens area. Remap to FarDepthValue so + // mask pixels do not cast false shadows. + depths.x = lerp(depths.x, 1.0, (float)(depths.x == 0)); // Stencil area + depths.y = lerp(depths.y, 1.0, (float)(depths.y == 0)); // Stencil area +#else depths.x = inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord, 0); depths.y = inParameters.DepthTexture.SampleLevel(inParameters.PointBorderSampler, coord_with_offset, 0); +#endif // Depth thresholds (bilinear/shadow thickness) are based on a fractional ratio of the difference between sampled depth and the far clip depth static const half kDepthThicknessFloor = 1e-4h; // Prevents division by zero in depth_scale when depth is at the far clip plane @@ -305,6 +332,19 @@ void WriteScreenSpaceShadow(DispatchParameters inParameters, int3 inGroupID, int // Sync wavefronts now groupshared DepthData is written GroupMemoryBarrierWithGroupSync(); +#if defined(VR) + // Check if the pixel we're writing to is on the correct eye side + half writeX = write_xy.x * inParameters.InvDepthTextureSize.x; + +# if defined(RIGHT) + if (writeX < 0.0) + return; +# else + if (writeX > 1.0) + return; +# endif +#endif + half start_depth = sampling_depth[0]; if (start_depth == 0.0 || start_depth == 1.0) diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli index 39281513cb..6fb969ff35 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/Burley.hlsli @@ -48,7 +48,7 @@ float3 GetScalingFactor(float3 albedo) return 3.5f + 100.f * pow(abs(value), 4); } -float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, float sssAmount, bool humanProfile) +float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, uint eyeIndex, float sssAmount, bool humanProfile) { float centerDepth = SharedData::GetScreenDepth(DepthTexture[DTid].x); @@ -71,7 +71,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, float sssAmount, bool hum float3 d3d = diffuseMeanFreePath.xyz * dmfpForSampling / s3d; const float3 normalVS = GBuffer::DecodeNormal(NormalTexture[DTid].xy); - const float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(normalVS, 0)).xyz); + const float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); float3 weightSum = 0.0f; float3 colorSum = 0.0f; @@ -115,7 +115,7 @@ float4 BurleyNormalizedSS(uint2 DTid, float2 texCoord, float sssAmount, bool hum float3 sampleColor = ColorTexture[samplePixcoord].xyz * maskSample; float sampleDepth = SharedData::GetScreenDepth(DepthTexture[samplePixcoord].x); float3 sampleNormalVS = GBuffer::DecodeNormal(NormalTexture[samplePixcoord].xy); - float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(sampleNormalVS, 0)).xyz); + float3 sampleNormalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(sampleNormalVS, 0)).xyz); float deltaDepth = (sampleDepth - centerDepth) * 10.f / GAME_UNIT_TO_CM; // convert to mm float radiusSampledInMM = sqrt(radius * radius + deltaDepth * deltaDepth); diff --git a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl index 7b595fc74d..e6457a05af 100644 --- a/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl +++ b/features/Subsurface Scattering/Shaders/SubsurfaceScattering/SeparableSSSCS.hlsl @@ -25,6 +25,7 @@ SamplerState PointSampler : register(s0); return; float2 texCoord = (DTid.xy + 0.5) * SharedData::BufferDim.zw; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(texCoord); #if defined(BURLEY) @@ -33,7 +34,7 @@ SamplerState PointSampler : register(s0); if (sssAmount > 0.0) { bool humanProfile = MaskTexture[DTid.xy].y > 0.0; - float4 color = BurleyNormalizedSS(DTid.xy, texCoord, sssAmount, humanProfile); + float4 color = BurleyNormalizedSS(DTid.xy, texCoord, eyeIndex, sssAmount, humanProfile); SSSRW[DTid.xy] = max(0, color); } diff --git a/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl b/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl new file mode 100644 index 0000000000..df107d9175 --- /dev/null +++ b/features/Upscaling/Shaders/Upscaling/ClearHMDMaskCS.hlsl @@ -0,0 +1,23 @@ +// Zeros color in the HMD hidden area per eye. +// Prevents DLSS/FSR from temporally accumulating the engine's sky/ambient clear color +// into visible pixels during head movement ("light blue border" ghosting). +// depth == 0.0 is the unrendered/hidden area value (Skyrim reversed-Z: far plane = 0). +// DepthIn is the combined stereo depth buffer; DepthOffsetX selects the eye's half. +// ColorInOut is the isolated per-eye buffer; ColorOffsetX is always 0. + +cbuffer ClearHMDMaskCB : register(b0) +{ + uint DepthOffsetX; // X offset into combined stereo depth (0 = left, eyeWidth = right) + uint ColorOffsetX; // X offset into color target (always 0 for per-eye buffers) + uint pad0; + uint pad1; +}; + +Texture2D DepthIn : register(t0); +RWTexture2D ColorInOut : register(u0); + +[numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { + // Read from stereo depth, write to potentially stereo color + if (DepthIn[dispatchID.xy + uint2(DepthOffsetX, 0)] == 0.0) + ColorInOut[dispatchID.xy + uint2(ColorOffsetX, 0)] = float4(0.0, 0.0, 0.0, 0.0); +} diff --git a/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl index 1a88fd3530..2788b913cf 100644 --- a/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/DepthRefractionUpscalePS.hlsl @@ -18,6 +18,10 @@ SamplerState LinearSampler : register(s0); Texture2D RefractionNormals : register(t0); Texture2D DepthTex : register(t1); +# if defined(VR) +Texture2D StencilTex : register(t2); +# endif + cbuffer JitterCB : register(b0) { float2 jitter; @@ -55,14 +59,36 @@ PS_OUTPUT main(PS_INPUT input) // Remove jitter offset to get the correct sampling coordinates float2 uv = originalUV - (jitter * SharedData::BufferDim.zw); - // Clamp within dynamic-resolution bounds. + // Clamp within dynamic-resolution bounds (VR: preserve per-eye bounds). uv = FrameBuffer::ClampDynamicResolutionAdjustedScreenPosition(uv, input.TexCoord); +# if defined(VR) + uint4 stencilSamples = StencilTex.GatherRed(LinearSampler, uv); + + // Choose the minimum stencil value + uint minStencil = min(min(stencilSamples.x, stencilSamples.y), min(stencilSamples.z, stencilSamples.w)); + + // Only write depth/stencil that is inside the viewable area + if (minStencil > 0x00) + discard; +# endif + // Upscale using linear sampling psout.RefractionNormals = RefractionNormals.SampleLevel(LinearSampler, uv, 0); psout.Depth = DepthTex.SampleLevel(LinearSampler, uv, 0); +# if defined(VR) + float bilinearDepth = psout.Depth; + if (useWideKernel > 0.5f) { + psout.Depth = SampleMinDepthWideGather(uv); + } else { + psout.Depth = SampleMinDepth2x2(uv); + } + // Keep SAO camera Z smooth to avoid over-occlusion; depth culling uses SV_Depth. + psout.SAOCameraZ = bilinearDepth; +# else psout.SAOCameraZ = psout.Depth; +# endif return psout; } diff --git a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl index 94d48eb554..34ea8250a8 100644 --- a/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/EncodeTexturesCS.hlsl @@ -2,8 +2,9 @@ cbuffer UpscalingData : register(b0) { - float2 TrueSamplingDim; - float2 pad0; + float2 TrueSamplingDim; // per-eye render dim in VR, full render dim otherwise + uint EyeOffsetX; // X offset into stereo source buffers; 0 for non-VR / left eye + uint pad0; }; Texture2D TAAMask : register(t0); @@ -19,19 +20,22 @@ RWTexture2D DepthOutput : register(u3); #endif [numthreads(8, 8, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { - // Bounds check + // Bounds check in per-eye space; EyeOffsetX=0 makes this identical to the old path for non-VR if (any(dispatchID.xy >= uint2(TrueSamplingDim))) return; - float2 taaMask = TAAMask[dispatchID.xy]; - float transparencyCompositionMask = NormalsWaterMask[dispatchID.xy].z; + // All source reads are in full stereo space; outputs are 0-based (per-eye or full-frame) + uint2 srcCoord = dispatchID.xy + uint2(EyeOffsetX, 0); + + float2 taaMask = TAAMask[srcCoord]; + float transparencyCompositionMask = NormalsWaterMask[srcCoord].z; #if defined(DLSS) - float depth = DepthMask[dispatchID.xy]; + float depth = DepthMask[srcCoord]; float nearFactor = smoothstep(4096.0 * 2.5, 0.0, SharedData::GetScreenDepth(depth)); // Find longest motion vector in 5x5 neighborhood - float2 motionVector = MotionVectorMask[dispatchID.xy]; + float2 motionVector = MotionVectorMask[srcCoord]; float2 longestMotionVector = motionVector; float maxMotionLengthSq = dot(motionVector, motionVector); @@ -41,15 +45,18 @@ RWTexture2D DepthOutput : register(u3); { int2 samplePos = int2(dispatchID.xy) + int2(x, y); - // Bounds check + // Bounds check stays in per-eye space — prevents cross-eye contamination in VR + // and out-of-bounds reads in non-VR (EyeOffsetX=0 makes these equivalent) if (any(samplePos < 0) || any(samplePos >= int2(TrueSamplingDim))) continue; - float neighborDepth = DepthMask[samplePos]; + // Source read uses full stereo offset + int2 srcPos = samplePos + int2(EyeOffsetX, 0); + float neighborDepth = DepthMask[srcPos]; // Take neighbor if it's longer AND closer if (neighborDepth < depth) { - float2 neighborMotionVector = MotionVectorMask[samplePos]; + float2 neighborMotionVector = MotionVectorMask[srcPos]; // Square motion vector for length float motionLengthSq = dot(neighborMotionVector, neighborMotionVector); @@ -67,12 +74,12 @@ RWTexture2D DepthOutput : register(u3); #if defined(DEPTH_OUTPUT) // Copy depth as R32_FLOAT so FSR DX11 backend receives a typed format. - // The raw depth resource is R24G8_TYPELESS which maps to FFX_SURFACE_FORMAT_UNKNOWN. - DepthOutput[dispatchID.xy] = DepthMask[dispatchID.xy]; + // The raw depth resource is R24G8_TYPELESS in VR which maps to FFX_SURFACE_FORMAT_UNKNOWN. + DepthOutput[dispatchID.xy] = DepthMask[srcCoord]; #endif float reactiveMask = taaMask.x * 0.1 + taaMask.y; ReactiveMask[dispatchID.xy] = reactiveMask; TransparencyCompositionMask[dispatchID.xy] = transparencyCompositionMask; -} +} \ No newline at end of file diff --git a/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl b/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl index 37386b8438..213ccd6931 100644 --- a/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl +++ b/features/Upscaling/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl @@ -15,6 +15,9 @@ struct PS_OUTPUT SamplerState LinearSampler : register(s0); Texture2D UnderwaterMask : register(t0); +# if defined(VR) +Texture2D SceneDepth : register(t1); +# endif cbuffer JitterCB : register(b0) { @@ -35,6 +38,95 @@ PS_OUTPUT main(PS_INPUT input) // Clamp within bounds uv = clamp(uv, 0.0, FrameBuffer::DynamicResolutionParams1.xy); +# if defined(VR) + // In VR the vanilla waterline draw (DrawIndexedInstanced, 2 instances) emits + // identical left-eye clip positions for both instances. The internal-res mask + // therefore only represents the left eye: the right-eye half of the buffer + // contains the tapered apex of the left-eye polygon, which is nearly all black. + // GetDynamicResolutionAdjustedScreenPosition then samples that black region for + // the right eye, making the entire right-eye underwater fog incorrect. + // + // Fix: reconstruct the mask analytically per-eye. For a horizontal water plane + // at height waterHeight, a pixel is "underwater" (mask = 1) when: + // - the camera itself is below the water surface, OR + // - the ray from the per-eye camera through this pixel points downward + // (rayDir.z < 0), meaning it looks below the water plane. + // This exactly reproduces what the vanilla waterline polygon approximates, + // but correctly per-eye. + + uint eyeIndex = (input.TexCoord.x >= 0.5) ? 1 : 0; + + // WaterData is a 5×5 grid centered on the camera; tile 12 (row 2, col 2) is + // always the camera's own tile. Pass eyeIndex so GetWaterData corrects the .w + // (water surface height) from eye-0 camera-relative Z into the current eye's frame. + // GetWaterData expects a camera-relative XY position; float3(0,0,0) is the camera + // itself, which always maps to the center tile (12). + float waterHeight = SharedData::GetWaterData(float3(0, 0, 0), eyeIndex).w; + + // Tile sentinel: try TESWaterSystem fallback. WaterSystemHeight is valid only when + // playerUnderwater == true (fully submerged); it is stored eye-0 camera-relative so + // the same per-eye correction as GetWaterData applies. + if (waterHeight <= WATER_HEIGHT_NO_TILE_SENTINEL) { + float sysHeight = SharedData::WaterSystemHeight; + if (sysHeight > WATER_HEIGHT_NO_TILE_SENTINEL) + waterHeight = sysHeight + FrameBuffer::CameraPosAdjust[0].z - FrameBuffer::CameraPosAdjust[eyeIndex].z; + } + + // GetWaterData returns INT_MIN (~-2.147e9) when the tile is outside the 5x5 grid. + if (waterHeight > WATER_HEIGHT_NO_TILE_SENTINEL) { + // Unpack from side-by-side stereo layout to per-eye UV [0, 1] + float2 eyeUV = float2(input.TexCoord.x * 2.0 - (float)eyeIndex, input.TexCoord.y); + + // Convert to NDC [-1, 1]. UV y=0 is the top of the screen; NDC y=+1 is the top. + float2 ndc = float2(eyeUV.x * 2.0 - 1.0, 1.0 - eyeUV.y * 2.0); + + // Sample depth using the shared de-jittered stereo UV (already DR-adjusted above). + // uv is in stereo space so no ConvertUVToSampleCoord round-trip is needed. + float depth = SceneDepth.Load(int3(uv * SharedData::BufferDim.xy, 0)).x; + + if (depth > EPSILON_DEPTH_SKY) { + // Geometry pixel: reconstruct world position from depth. + // CameraViewProjInverse[eyeIndex] maps clip-space back to the per-eye + // camera-relative world space. waterHeight has been adjusted to the same + // frame, so the comparison is correct for both eyes. + float4 worldPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4(ndc, depth, 1.0)); + worldPos /= worldPos.w; + // kSurfaceBias (Skyrim world units, ~1 unit ≈ 1.4 cm) anchors the mask + // threshold relative to the flat waterHeight plane to absorb wave-vertex + // displacement (measured max trough ≈ 2.92 units; 3.5 gives margin). + // + // The threshold direction depends on view orientation: + // Looking UP (worldPos.z > 0, pixel above camera in world space): + // Camera is below the surface viewing it from underneath. + // Expand threshold upward by +kSurfaceBias so the entire wave surface + // (crests and troughs alike) is included in the masked region. + // Looking DOWN (worldPos.z <= 0, pixel below or level with camera): + // The surface is seen from above or the camera is above water. + // Shrink threshold downward by -kSurfaceBias so the surface itself + // is excluded from the mask (no fog on the surface seen from above). + static const float kSurfaceBias = 3.5; + bool lookingUp = worldPos.z > 0.0; + bool cameraUnderwater = waterHeight > 0.0; + float threshold = (cameraUnderwater && lookingUp) ? waterHeight + kSurfaceBias : waterHeight - kSurfaceBias; + psout.UnderwaterMask = (worldPos.z < threshold) ? 1.0 : 0.0; + } else { + // depth <= EPSILON_DEPTH_SKY: sky / unrendered pixels (reversed-Z depth clear value). + // Unproject to obtain the per-pixel ray direction and decide based on that. + float4 worldFarPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4(ndc, 0.0, 1.0)); + worldFarPos /= worldFarPos.w; + float3 rayDir = normalize(worldFarPos.xyz); + // Per-eye waterHeight > 0 means the water surface is above THIS eye's camera + // (eye is below water); <= 0 means the eye camera is above the water surface. + psout.UnderwaterMask = (waterHeight > 0.0 || rayDir.z < 0.0) ? 1.0 : 0.0; + } + return psout; + } + // No water tile or system height available: fall through to the standard sampler path. + // The left-eye result from the vanilla mask is still accurate here; the right-eye + // will be approximate, but both sources failing implies no nearby water so the + // visual impact is nil. +# endif + // Upscale using linear sampling with jitter-corrected coordinates psout.UnderwaterMask = UnderwaterMask.SampleLevel(LinearSampler, uv, 0); diff --git a/features/VR/CORE b/features/VR/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli index f144c09277..9f002d4723 100644 --- a/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli +++ b/features/Volumetric Shadows/Shaders/VolumetricShadows/VolumetricShadows.hlsli @@ -57,17 +57,17 @@ namespace VolumetricShadows return shadow * rcpSampleCount; } - float GetVSMShadow3D(float3 startPosition, float3 endPosition, float noise, uint baseSampleCount, out float surfaceShadow) + float GetVSMShadow3D(float3 startPosition, float3 endPosition, float noise, uint baseSampleCount, uint eyeIndex, out float surfaceShadow) { DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; // View-space z — matches the linear cascade split distances from BSShadowDirectionalLight. float3 midPosition = (startPosition + endPosition) * 0.5; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(midPosition)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(midPosition, eyeIndex)); // Cascade projections are world-space; positions come in camera-relative. - startPosition += FrameBuffer::CameraPosAdjust.xyz; - endPosition += FrameBuffer::CameraPosAdjust.xyz; + startPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; + endPosition += FrameBuffer::CameraPosAdjust[eyeIndex].xyz; // Early out beyond cascade range if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) { @@ -134,7 +134,7 @@ namespace VolumetricShadows { DirectionalShadowLightData directionalShadowLightData = DirectionalShadowLights[0]; - float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(position)); + float shadowMapDepth = SharedData::GetScreenDepth(FrameBuffer::GetShadowDepth(position, eyeIndex)); // Early out beyond cascade range if (shadowMapDepth >= directionalShadowLightData.EndSplitDistances.y) { diff --git a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli index caa6b41bf9..7a9d793453 100644 --- a/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli +++ b/features/Water Effects/Shaders/WaterEffects/WaterCaustics.hlsli @@ -23,7 +23,7 @@ namespace WaterEffects return lerp(center.xxx, dispersed, 0.5); } - float3 ComputeCaustics(float4 waterData, float3 worldPosition) + float3 ComputeCaustics(float4 waterData, float3 worldPosition, uint eyeIndex) { float causticsDistToWater = waterData.w - worldPosition.z; float shoreFactorCaustics = saturate(causticsDistToWater / 64.0); @@ -32,7 +32,7 @@ namespace WaterEffects float causticsFade = 1.0 - saturate(causticsDistToWater / 1024.0); causticsFade *= causticsFade; - float2 causticsUV = (worldPosition.xy + FrameBuffer::CameraPosAdjust.xy) * 0.005; + float2 causticsUV = (worldPosition.xy + FrameBuffer::CameraPosAdjust[eyeIndex].xy) * 0.005; float2 dispersionOffset = float2(0.6, 0.8) * (0.025 * shoreFactorCaustics * saturate(causticsDistToWater / 256.0)); float2 causticsUV1 = PanCausticsUV(causticsUV, 0.5 * 0.2, 1.0); diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index d6c294423c..4d6b3de2dd 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -546,6 +546,7 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = Tailwind (wind behind player)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = Crosswind (left/right)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = Headwind (wind coming toward player)", + "feature.dynamic_cubemaps.advanced_vr_settings": "Advanced VR Settings", "feature.dynamic_cubemaps.color": "Color", "feature.dynamic_cubemaps.creator_info": "You must enable creator mode by adding the shader define CREATOR", "feature.dynamic_cubemaps.description": "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces.", @@ -557,10 +558,12 @@ "feature.dynamic_cubemaps.key_feature_1": "Real-time environment capture for realistic reflections", "feature.dynamic_cubemaps.key_feature_2": "Dynamic cube map generation based on camera position", "feature.dynamic_cubemaps.key_feature_3": "Enhanced water reflections with environmental details", - "feature.dynamic_cubemaps.key_feature_4": "Optimized cubemap inference and irradiance calculation", + "feature.dynamic_cubemaps.key_feature_4": "Support for both standard and VR rendering modes", + "feature.dynamic_cubemaps.key_feature_5": "Optimized cubemap inference and irradiance calculation", "feature.dynamic_cubemaps.name": "Dynamic Cubemaps", "feature.dynamic_cubemaps.roughness": "Roughness", "feature.dynamic_cubemaps.screen_space_reflections": "Screen Space Reflections", + "feature.dynamic_cubemaps.vr_restart_required": "A restart is required to enable in VR. Save Settings after enabling and restart the game.", "feature.exp_height_fog.apply_vanilla_fade": "Apply Vanilla Fade", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "Applies vanilla fade brightness to exponential height fog.", "feature.exp_height_fog.cubemap_mip_level": "Cubemap Mip Level", @@ -856,6 +859,7 @@ "feature.light_limit_fix.key_feature_2": "Unlimited dynamic lights", "feature.light_limit_fix.key_feature_3": "Improved lighting quality", "feature.light_limit_fix.key_feature_4": "Enhanced visual realism", + "feature.light_limit_fix.key_feature_5": "Enhanced visual realism", "feature.light_limit_fix.light_limit_vis": "Light Limit Visualization", "feature.light_limit_fix.lights_vis_mode": "Lights Visualisation Mode", "feature.light_limit_fix.lights_vis_mode_tooltip": " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n - Visualise the number of strict lights.\n - Visualise the number of clustered lights.\n - Visualize the Shadow Mask.\n", @@ -1054,9 +1058,11 @@ "feature.screen_space_gi.toggles": "Toggles", "feature.screen_space_gi.vanilla_ssao": "Vanilla SSAO", "feature.screen_space_gi.vanilla_ssao_tooltip": "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.", + "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "Vanilla SSAO is not supported in VR.", "feature.screen_space_gi.view_resize": "View Resize", "feature.screen_space_gi.visual": "Visual", "feature.screen_space_gi.visual_il": "Visual - IL", + "feature.screen_space_gi.vr_warning": "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects.", "feature.screen_space_shadows.bilinear_threshold": "Bilinear Threshold", "feature.screen_space_shadows.bilinear_threshold_tooltip": "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.", "feature.screen_space_shadows.description": "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\nThis technique adds fine-detail shadows that traditional shadow mapping might miss.", @@ -1075,6 +1081,8 @@ "feature.screen_space_shadows.shadow_contrast_tooltip": "Contrast boost for the shadow transition. Higher values produce harder shadow edges.", "feature.screen_space_shadows.surface_thickness": "Surface Thickness", "feature.screen_space_shadows.surface_thickness_tooltip": "Assumed thickness of surfaces for shadow detection. Lower values produce thinner, more precise shadows.", + "feature.screen_space_shadows.vr_stereo_sync": "VR Stereo Sync", + "feature.screen_space_shadows.vr_stereo_sync_tooltip": "Synchronizes shadow data between left and right eyes via bilateral reprojection and applies a depth-weighted blur to reduce per-eye noise. Uses min-blend so if either eye detects an occluder, the shadow is preserved. ", "feature.screenshot.apply_crop": "Apply crop", "feature.screenshot.async_note": "Capture and save run asynchronously without stalling the game.", "feature.screenshot.crop": "Crop", @@ -1088,7 +1096,7 @@ "feature.screenshot.name": "Screenshot", "feature.screenshot.open": "Open", "feature.screenshot.output": "Output", - "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR captures use the lossless format selected below.", + "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR and VR captures use the lossless format selected below.", "feature.screenshot.take_screenshot": "Take Screenshot Now", "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "A multiplier for the vanilla specular map, applied to the first layer's roughness", "feature.skin.adds_a_constant_layer_of_wetness_to_all": "Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort.", @@ -1353,6 +1361,7 @@ "feature.upscaling.method_none": "None", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "Upscaling", + "feature.upscaling.native_inputs": "Native Inputs", "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", "feature.upscaling.preset_balanced": "Balanced", "feature.upscaling.preset_dlaa": "DLAA", @@ -1367,12 +1376,15 @@ "feature.upscaling.streamline_logging_restart_note": "Changing this requires a restart to take effect.", "feature.upscaling.streamline_logging_tooltip": "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.", "feature.upscaling.upscale_preset": "Upscale Preset", + "feature.upscaling.upscaling_intermediates": "Upscaling Intermediates", "feature.upscaling.use_fps_limit": "Use FPS Limit", "feature.upscaling.use_fps_limit_tooltip_1": "Uses Reflex's internal FPS cap for steadier frametimes.", "feature.upscaling.use_fps_limit_tooltip_2": "Can lower latency versus uncapped rendering.", "feature.upscaling.use_markers_to_optimize": "Use Markers To Optimize", "feature.upscaling.use_markers_to_optimize_tooltip_1": "Uses frame markers for tighter Reflex timing.", "feature.upscaling.use_markers_to_optimize_tooltip_2": "Try On first; turn Off if it causes stutter on your setup.", + "feature.upscaling.view_resize": "View Resize", + "feature.upscaling.vr_intermediates_not_created": "VR intermediates not yet created (enter game world)", "feature.volumetric_lighting.description": "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\nThis adds dramatic god rays and atmospheric depth to both interior and exterior environments.", "feature.volumetric_lighting.enable_exteriors": "Enable Volumetric Lighting in Exteriors", "feature.volumetric_lighting.enable_interiors": "Enable Volumetric Lighting in Interiors", @@ -1400,6 +1412,31 @@ "feature.volumetric_shadows.key_feature_3": "Multi-cascade support", "feature.volumetric_shadows.key_feature_4": "Optimized for effects rendering", "feature.volumetric_shadows.name": "Volumetric Shadows", + "feature.vr.description": "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", + "feature.vr.key_feature_1": "Depth buffer culling optimization for VR performance", + "feature.vr.key_feature_2": "In-scene overlay menu with HMD/Controller/Fixed World attach modes", + "feature.vr.key_feature_3": "VR controller input with customizable button mappings", + "feature.vr.key_feature_4": "Grip-to-drag overlay positioning with depth control", + "feature.vr.key_feature_5": "Configurable occlusion culling parameters", + "feature.vr.key_feature_6": "Enhanced VR compatibility with SteamVR and OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "Debug", + "feature.vr_stereo.debug_pom_depth": "Debug POM Depth", + "feature.vr_stereo.disocclusion_depth_threshold": "Disocclusion Depth Threshold", + "feature.vr_stereo.enable": "Enable", + "feature.vr_stereo.enable_stereo_reprojection": "Enable Stereo Reprojection", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.", + "feature.vr_stereo.forward_occlusion_scale": "Forward Occlusion Scale", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.", + "feature.vr_stereo.full_blend_depth_view": "Full Blend Depth View", + "feature.vr_stereo.full_blend_distance": "Full Blend Distance", + "feature.vr_stereo.full_blend_distance_tooltip": "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.", + "feature.vr_stereo.full_blend_zone_hint": " Cyan = full blend zone (closer = stronger tint)", + "feature.vr_stereo.off": "Off", + "feature.vr_stereo.pom_depth_scale": "POM Depth Scale", + "feature.vr_stereo.pom_depth_scale_tooltip": "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.", + "feature.vr_stereo.restart_required": "Restart is required to enable VR stereo reprojection.", + "feature.vr_stereo.skip_pixel_reprojection": "Skip Pixel Reprojection", "feature.water_effects.description": "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\nThis feature adds dynamic light patterns and improved water visual quality.", "feature.water_effects.key_feature_1": "Realistic water caustics", "feature.water_effects.key_feature_2": "Enhanced underwater lighting", diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index a5ce43434a..30264ab701 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -545,6 +545,7 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = 顺风(风在玩家背后)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = 横风(左/右)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = 逆风(风朝向玩家)", + "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", "feature.dynamic_cubemaps.color": "颜色", "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", @@ -556,10 +557,12 @@ "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", "feature.dynamic_cubemaps.key_feature_3": "增强的水面反射,包含环境细节", - "feature.dynamic_cubemaps.key_feature_4": "优化的立方体贴图推理与辐照度计算", + "feature.dynamic_cubemaps.key_feature_4": "支持标准和VR两种渲染模式", + "feature.dynamic_cubemaps.key_feature_5": "优化的立方体贴图推理与辐照度计算", "feature.dynamic_cubemaps.name": "动态立方体贴图", "feature.dynamic_cubemaps.roughness": "粗糙度", "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", + "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", @@ -855,6 +858,7 @@ "feature.light_limit_fix.key_feature_2": "无限动态光源", "feature.light_limit_fix.key_feature_3": "提升光照质量", "feature.light_limit_fix.key_feature_4": "增强视觉真实感", + "feature.light_limit_fix.key_feature_5": "增强视觉真实感", "feature.light_limit_fix.light_limit_vis": "光源限制可视化", "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", @@ -1053,9 +1057,11 @@ "feature.screen_space_gi.toggles": "开关", "feature.screen_space_gi.vanilla_ssao": "原版SSAO", "feature.screen_space_gi.vanilla_ssao_tooltip": "启用Skyrim内置的SSAO。使用SSGI时通常禁用此选项以避免双重变暗。", + "feature.screen_space_gi.vanilla_ssao_tooltip_vr": "VR不支持原版SSAO。", "feature.screen_space_gi.view_resize": "视图调整大小", "feature.screen_space_gi.visual": "视觉", "feature.screen_space_gi.visual_il": "视觉 - IL", + "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", @@ -1074,6 +1080,8 @@ "feature.screen_space_shadows.shadow_contrast_tooltip": "阴影过渡的对比度增强。较高值产生更硬的阴影边缘。", "feature.screen_space_shadows.surface_thickness": "表面厚度", "feature.screen_space_shadows.surface_thickness_tooltip": "阴影检测的假设表面厚度。较低值产生更薄、更精确的阴影。", + "feature.screen_space_shadows.vr_stereo_sync": "VR立体同步", + "feature.screen_space_shadows.vr_stereo_sync_tooltip": "通过双向重投影同步左右眼之间的阴影数据,并应用深度加权模糊以减少每只眼睛的噪点。使用最小混合,如果任一眼睛检测到遮挡物,则保留阴影。", "feature.screenshot.apply_crop": "应用裁剪", "feature.screenshot.async_note": "捕获和保存异步运行,不会让游戏停顿。", "feature.screenshot.crop": "裁剪", @@ -1352,6 +1360,7 @@ "feature.upscaling.method_none": "无", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "超分辨率", + "feature.upscaling.native_inputs": "原生输入", "feature.upscaling.nvidia_reflex": "NVIDIA Reflex", "feature.upscaling.preset_balanced": "平衡", "feature.upscaling.preset_dlaa": "DLAA", @@ -1366,12 +1375,15 @@ "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", "feature.upscaling.upscale_preset": "升频预设", + "feature.upscaling.upscaling_intermediates": "升频中间结果", "feature.upscaling.use_fps_limit": "使用FPS限制", "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", "feature.upscaling.use_fps_limit_tooltip_2": "相比无上限渲染可以降低延迟。", "feature.upscaling.use_markers_to_optimize": "使用标记优化", "feature.upscaling.use_markers_to_optimize_tooltip_1": "使用帧标记以实现更紧密的Reflex计时。", "feature.upscaling.use_markers_to_optimize_tooltip_2": "先尝试开启;如果在您的设置上造成卡顿则关闭。", + "feature.upscaling.view_resize": "视图调整大小", + "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", @@ -1399,6 +1411,31 @@ "feature.volumetric_shadows.key_feature_3": "多级联支持", "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", "feature.volumetric_shadows.name": "体积阴影", + "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", + "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", + "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", + "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", + "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", + "feature.vr.key_feature_5": "可配置的遮挡剔除参数", + "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "调试", + "feature.vr_stereo.debug_pom_depth": "调试POM深度", + "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", + "feature.vr_stereo.enable": "启用", + "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", + "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", + "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", + "feature.vr_stereo.full_blend_distance": "完全混合距离", + "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", + "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", + "feature.vr_stereo.off": "关闭", + "feature.vr_stereo.pom_depth_scale": "POM深度缩放", + "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", + "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", + "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", "feature.water_effects.key_feature_1": "逼真的水面焦散", "feature.water_effects.key_feature_2": "增强的水下光照", diff --git a/package/Shaders/Common/FrameBuffer.hlsli b/package/Shaders/Common/FrameBuffer.hlsli index ebe29d3981..68f0f90371 100644 --- a/package/Shaders/Common/FrameBuffer.hlsli +++ b/package/Shaders/Common/FrameBuffer.hlsli @@ -6,18 +6,19 @@ namespace FrameBuffer cbuffer PerFrame : register(b12) { - row_major float4x4 CameraView : packoffset(c0); - row_major float4x4 CameraProj : packoffset(c4); - row_major float4x4 CameraViewProj : packoffset(c8); - row_major float4x4 CameraViewProjUnjittered : packoffset(c12); - row_major float4x4 CameraPreviousViewProjUnjittered : packoffset(c16); - row_major float4x4 CameraProjUnjittered : packoffset(c20); - row_major float4x4 CameraProjUnjitteredInverse : packoffset(c24); - row_major float4x4 CameraViewInverse : packoffset(c28); - row_major float4x4 CameraViewProjInverse : packoffset(c32); - row_major float4x4 CameraProjInverse : packoffset(c36); - float4 CameraPosAdjust : packoffset(c40); - float4 CameraPreviousPosAdjust : packoffset(c41); // fDRClampOffset in w +#if !defined(VR) + row_major float4x4 CameraView[1] : packoffset(c0); + row_major float4x4 CameraProj[1] : packoffset(c4); + row_major float4x4 CameraViewProj[1] : packoffset(c8); + row_major float4x4 CameraViewProjUnjittered[1] : packoffset(c12); + row_major float4x4 CameraPreviousViewProjUnjittered[1] : packoffset(c16); + row_major float4x4 CameraProjUnjittered[1] : packoffset(c20); + row_major float4x4 CameraProjUnjitteredInverse[1] : packoffset(c24); + row_major float4x4 CameraViewInverse[1] : packoffset(c28); + row_major float4x4 CameraViewProjInverse[1] : packoffset(c32); + row_major float4x4 CameraProjInverse[1] : packoffset(c36); + float4 CameraPosAdjust[1] : packoffset(c40); + float4 CameraPreviousPosAdjust[1] : packoffset(c41); // fDRClampOffset in w float4 FrameParams : packoffset(c42); // inverse fGamma in x, some flags in yzw float4 DynamicResolutionParams1 : packoffset(c43); // fDynamicResolutionWidthRatio in x, // fDynamicResolutionHeightRatio in y, @@ -27,6 +28,29 @@ namespace FrameBuffer // fDynamicResolutionHeightRatio in y, // fDynamicResolutionWidthRatio - fDRClampOffset in z, // fDynamicResolutionPreviousWidthRatio - fDRClampOffset in w +#else + row_major float4x4 CameraView[2] : packoffset(c0); + row_major float4x4 CameraProj[2] : packoffset(c8); + row_major float4x4 CameraViewProj[2] : packoffset(c16); + row_major float4x4 CameraViewProjUnjittered[2] : packoffset(c24); + row_major float4x4 CameraPreviousViewProjUnjittered[2] : packoffset(c32); + row_major float4x4 CameraProjUnjittered[2] : packoffset(c40); + row_major float4x4 CameraProjUnjitteredInverse[2] : packoffset(c48); + row_major float4x4 CameraViewInverse[2] : packoffset(c56); + row_major float4x4 CameraViewProjInverse[2] : packoffset(c64); + row_major float4x4 CameraProjInverse[2] : packoffset(c72); + float4 CameraPosAdjust[2] : packoffset(c80); + float4 CameraPreviousPosAdjust[2] : packoffset(c82); // fDRClampOffset in w + float4 FrameParams : packoffset(c84); // inverse fGamma in x, some flags in yzw + float4 DynamicResolutionParams1 : packoffset(c85); // fDynamicResolutionWidthRatio in x, + // fDynamicResolutionHeightRatio in y, + // fDynamicResolutionPreviousWidthRatio in z, + // fDynamicResolutionPreviousHeightRatio in w + float4 DynamicResolutionParams2 : packoffset(c86); // inverse fDynamicResolutionWidthRatio in x, inverse + // fDynamicResolutionHeightRatio in y, + // fDynamicResolutionWidthRatio - fDRClampOffset in z, + // fDynamicResolutionPreviousWidthRatio - fDRClampOffset in w +#endif // !VR } /** @@ -36,18 +60,32 @@ namespace FrameBuffer * space by custom math (for example, after jitter removal or other UV manipulation). * This function only clamps; it does not apply dynamic-resolution scaling. * + * In VR, clamping is restricted to the current eye half to avoid cross-eye sampling. + * * @param[in] screenPositionDR UVs already expressed in dynamic-resolution space. - * @param[in] screenPosition Original normalized screen UVs. + * @param[in] screenPosition Original normalized screen UVs (used to infer eye in VR). + * @param[in] stereo Whether to apply stereo eye-half clamping in VR. Default is 1. * @return Clamped dynamic-resolution UVs. */ - float2 ClampDynamicResolutionAdjustedScreenPosition(float2 screenPositionDR, float2 screenPosition) + float2 ClampDynamicResolutionAdjustedScreenPosition(float2 screenPositionDR, float2 screenPosition, uint stereo = 1) { float2 minValue = 0; float2 maxValue = float2(DynamicResolutionParams2.z, DynamicResolutionParams1.y); +#if defined(VR) + // VR uses side-by-side stereo packing in the shared render target. + // Clamp within the current eye's half to avoid cross-eye sampling. + if (stereo) { + bool isRight = screenPosition.x >= 0.5; + float minFactor = isRight ? 1 : 0; + minValue.x = 0.5 * (DynamicResolutionParams2.z * minFactor); + float maxFactor = isRight ? 2 : 1; + maxValue.x = 0.5 * (DynamicResolutionParams2.z * maxFactor); + } +#endif return clamp(screenPositionDR, minValue, maxValue); } - // Projects a world-space (camera-relative) point into NDC using CameraViewProj + // Projects a world-space (camera-relative) point into NDC using the eye's CameraViewProj // and returns the post-perspective z (NDC depth). Combine with SharedData::GetScreenDepth // to get a linear view-space distance suitable for cascade-split comparisons. float GetShadowDepth(float3 positionWS, uint eyeIndex) @@ -66,12 +104,13 @@ namespace FrameBuffer * `ClampDynamicResolutionAdjustedScreenPosition(...)` instead. * * @param[in] screenPosition Normalized screen UVs in non-DR space. + * @param[in] stereo Whether to apply stereo eye-half clamping in VR. Default is 1. * @return Dynamic-resolution-adjusted and clamped UVs. */ - float2 GetDynamicResolutionAdjustedScreenPosition(float2 screenPosition) + float2 GetDynamicResolutionAdjustedScreenPosition(float2 screenPosition, uint stereo = 1) { float2 screenPositionDR = DynamicResolutionParams1.xy * screenPosition; - return ClampDynamicResolutionAdjustedScreenPosition(screenPositionDR, screenPosition); + return ClampDynamicResolutionAdjustedScreenPosition(screenPositionDR, screenPosition, stereo); } /** @@ -79,9 +118,9 @@ namespace FrameBuffer * * Applies dynamic-resolution adjustment/clamp to XY and preserves Z unchanged. */ - float3 GetDynamicResolutionAdjustedScreenPosition(float3 screenPositionDR) + float3 GetDynamicResolutionAdjustedScreenPosition(float3 screenPositionDR, uint stereo = 1) { - return float3(GetDynamicResolutionAdjustedScreenPosition(screenPositionDR.xy), screenPositionDR.z); + return float3(GetDynamicResolutionAdjustedScreenPosition(screenPositionDR.xy, stereo), screenPositionDR.z); } /** @@ -107,6 +146,13 @@ namespace FrameBuffer float2 screenPositionDR = DynamicResolutionParams1.zw * screenPosition; float2 minValue = 0; float2 maxValue = float2(DynamicResolutionParams2.w, DynamicResolutionParams1.w); +#if defined(VR) + bool isRight = screenPosition.x >= 0.5; + float minFactor = isRight ? 1 : 0; + minValue.x = 0.5 * (DynamicResolutionParams2.w * minFactor); + float maxFactor = isRight ? 2 : 1; + maxValue.x = 0.5 * (DynamicResolutionParams2.w * maxFactor); +#endif return clamp(screenPositionDR, minValue, maxValue); } @@ -115,22 +161,22 @@ namespace FrameBuffer return pow(abs(linearColor), FrameParams.x); } - float3 WorldToView(float3 x, bool is_position = true) + float3 WorldToView(float3 x, bool is_position = true, uint a_eyeIndex = 0) { float4 newPosition = float4(x, (float)is_position); - return mul(CameraView, newPosition).xyz; + return mul(CameraView[a_eyeIndex], newPosition).xyz; } - float3 ViewToWorld(float3 x, bool is_position = true) + float3 ViewToWorld(float3 x, bool is_position = true, uint a_eyeIndex = 0) { float4 newPosition = float4(x, (float)is_position); - return mul(CameraViewInverse, newPosition).xyz; + return mul(CameraViewInverse[a_eyeIndex], newPosition).xyz; } - float2 ViewToUV(float3 x, bool is_position = true) + float2 ViewToUV(float3 x, bool is_position = true, uint a_eyeIndex = 0) { float4 newPosition = float4(x, (float)is_position); - float4 uv = mul(CameraProj, newPosition); + float4 uv = mul(CameraProj[a_eyeIndex], newPosition); return (uv.xy / uv.w) * float2(0.5f, -0.5f) + 0.5f; } diff --git a/package/Shaders/Common/MotionBlur.hlsli b/package/Shaders/Common/MotionBlur.hlsli index a6cca62ace..36ba16fb45 100644 --- a/package/Shaders/Common/MotionBlur.hlsli +++ b/package/Shaders/Common/MotionBlur.hlsli @@ -5,10 +5,10 @@ namespace MotionBlur { - float2 GetSSMotionVector(float4 a_wsPosition, float4 a_previousWSPosition) + float2 GetSSMotionVector(float4 a_wsPosition, float4 a_previousWSPosition, uint a_eyeIndex = 0) { - float4 screenPosition = mul(FrameBuffer::CameraViewProjUnjittered, a_wsPosition); - float4 previousScreenPosition = mul(FrameBuffer::CameraPreviousViewProjUnjittered, a_previousWSPosition); + float4 screenPosition = mul(FrameBuffer::CameraViewProjUnjittered[a_eyeIndex], a_wsPosition); + float4 previousScreenPosition = mul(FrameBuffer::CameraPreviousViewProjUnjittered[a_eyeIndex], a_previousWSPosition); screenPosition.xy = screenPosition.xy / screenPosition.ww; previousScreenPosition.xy = previousScreenPosition.xy / previousScreenPosition.ww; return float2(-0.5, 0.5) * (screenPosition.xy - previousScreenPosition.xy); diff --git a/package/Shaders/Common/ShadowSampling.hlsli b/package/Shaders/Common/ShadowSampling.hlsli index 427c7fb440..2dcba97856 100644 --- a/package/Shaders/Common/ShadowSampling.hlsli +++ b/package/Shaders/Common/ShadowSampling.hlsli @@ -55,7 +55,7 @@ namespace ShadowSampling return SharedData::HasDirectionalShadows; } - float GetWorldShadow(float3 positionWS, float3 offset) + float GetWorldShadow(float3 positionWS, float3 offset, uint eyeIndex) { if (SharedData::InInterior || SharedData::HideSky || SharedData::InMapMenu) return 1.0; @@ -72,7 +72,7 @@ namespace ShadowSampling return worldShadow; } - float Get3DFilteredShadow(float3 positionWS, float3 viewDirection, float2 screenPosition, out float surfaceShadow) + float Get3DFilteredShadow(float3 positionWS, float3 viewDirection, float2 screenPosition, uint eyeIndex, out float surfaceShadow) { #if defined(EFFECT) float viewRayLength = min(Permutation::EffectRadius * 0.2, 256); @@ -101,7 +101,7 @@ namespace ShadowSampling for (uint i = 0; i < sampleCount; i++) { float t = (float(i) + noise) * rcpSampleCount; float3 sampledPositionWS = lerp(endPosition, startPosition, t); - float worldShadowSample = ShadowSampling::GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust.xyz); + float worldShadowSample = ShadowSampling::GetWorldShadow(sampledPositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); surfaceShadow = worldShadowSample; worldShadow += worldShadowSample; } @@ -114,7 +114,7 @@ namespace ShadowSampling #if defined(VOLUMETRIC_SHADOWS) if (HasDirectionalShadows()) { float vsmSurfaceShadow; - float shadow = VolumetricShadows::GetVSMShadow3D(startPosition, endPosition, noise, sampleCount, vsmSurfaceShadow); + float shadow = VolumetricShadows::GetVSMShadow3D(startPosition, endPosition, noise, sampleCount, eyeIndex, vsmSurfaceShadow); surfaceShadow *= vsmSurfaceShadow; return worldShadow * shadow; } diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index b90f51f55a..acc7bb2f9d 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -3,6 +3,7 @@ #include "Common/FrameBuffer.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" +#include "Common/VR.hlsli" namespace SharedData { @@ -30,7 +31,7 @@ namespace SharedData bool InMapMenu; // If the world/local map is open (note that the renderer is still deferred here) bool HideSky; // HideSky flag in WorldSpace, e.g. Blackreach float MipBias; // Offset to mip level for TAA sharpness - float WaterSystemHeight; // TES::GetWaterHeight in camera-relative Z; -FLT_MAX when no water body found + float WaterSystemHeight; // TES::GetWaterHeight at eye-0 in camera-relative Z; -FLT_MAX when no water body found (VR only) float3 pad0; float4 AmbientSHR; float4 AmbientSHG; @@ -348,16 +349,17 @@ namespace SharedData Texture2D DepthTexture : register(t17); // Get a int3 to be used as texture sample coord. [0,1] in uv space - int3 ConvertUVToSampleCoord(float2 uv) + int3 ConvertUVToSampleCoord(float2 uv, uint a_eyeIndex) { + uv = Stereo::ConvertToStereoUV(uv, a_eyeIndex); uv = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); return int3(uv * BufferDim.xy, 0); } // Get a raw depth from the depth buffer. [0,1] in uv space - float GetDepth(float2 uv) + float GetDepth(float2 uv, uint a_eyeIndex = 0) { - return DepthTexture.Load(ConvertUVToSampleCoord(uv)).x; + return DepthTexture.Load(ConvertUVToSampleCoord(uv, a_eyeIndex)).x; } float GetScreenDepth(float depth) @@ -370,16 +372,19 @@ namespace SharedData return (CameraData.w / (-depths * CameraData.z + CameraData.x)); } - float GetScreenDepth(float2 uv) + float GetScreenDepth(float2 uv, uint a_eyeIndex = 0) { - float depth = GetDepth(uv); + float depth = GetDepth(uv, a_eyeIndex); return GetScreenDepth(depth); } // Returns water data for the tile containing worldPosition (camera-relative XY). - float4 GetWaterData(float3 worldPosition) + // The .w component (water surface height) is stored in C++ as camera-relative Z of + // eye 0 (left eye). Pass eyeIndex to have .w corrected into the current eye's + // camera-relative frame; defaults to 0 (no correction, backwards-compatible). + float4 GetWaterData(float3 worldPosition, uint eyeIndex = 0) { - float2 cellF = (((worldPosition.xy + FrameBuffer::CameraPosAdjust.xy)) / 4096.0) + 64.0; // always positive + float2 cellF = (((worldPosition.xy + FrameBuffer::CameraPosAdjust[0].xy)) / 4096.0) + 64.0; // always positive int2 cellInt; float2 cellFrac = modf(cellF, cellInt); @@ -395,6 +400,12 @@ namespace SharedData [flatten] if (cellInt.x < 5 && cellInt.x >= 0 && cellInt.y < 5 && cellInt.y >= 0) waterData = WaterData[waterTile]; +# if defined(VR) + // Correct .w from eye-0 camera-relative Z to the current eye's camera-relative Z. + // No-op when eyeIndex == 0 (both terms are identical). + waterData.w += FrameBuffer::CameraPosAdjust[0].z - FrameBuffer::CameraPosAdjust[eyeIndex].z; +# endif + return waterData; } diff --git a/package/Shaders/Common/VR.hlsli b/package/Shaders/Common/VR.hlsli new file mode 100644 index 0000000000..0b8ea117ba --- /dev/null +++ b/package/Shaders/Common/VR.hlsli @@ -0,0 +1,668 @@ +#ifndef __VR_DEPENDENCY_HLSL__ +#define __VR_DEPENDENCY_HLSL__ +#ifdef VR + +// First person model depth threshold for VR occlusion logic +# ifndef VR_FP_Z +# define VR_FP_Z 18.0 +# endif + +# if defined(VSHADER) +# include "Common/Math.hlsli" +# endif // VSHADER + +# if (!defined(COMPUTESHADER) && !defined(CSHADER)) || defined(FRAMEBUFFER) +# include "Common/FrameBuffer.hlsli" +# endif +cbuffer VRValues : register(b13) +{ + float AlphaTestRefRS : packoffset(c0); + float StereoEnabled : packoffset(c0.y); + float2 EyeOffsetScale : packoffset(c0.z); + float4 EyeClipEdge[2] : packoffset(c1); +} +#endif + +namespace Stereo +{ +#ifdef VR_STEREO_OPT + /// Sentinel written to PomOffsetTex when a pixel's Lighting PS did not run POM. + /// Convention: -1.0 = no POM; >= 0.0 = POM ran (StereoBlendCS detects by sign). + /// Must match kPomOffsetNoData in VRStereoOptimizations.h. + static const float POM_NO_DATA = -1.0; +#endif + /** + Converts to the eye specific uv [0,1]. + In VR, texture buffers include the left and right eye in the same buffer. Flat + only has a single camera for the entire width. This means the x value [0, .5] + represents the left eye, and the x value (.5, 1] are the right eye. This returns + the adjusted value + @param uv - uv coords [0,1] to be encoded for VR + @param a_eyeIndex The eyeIndex; 0 is left, 1 is right + @param a_invertY Whether to invert the Y direction + @returns uv with x coords adjusted for the VR texture buffer + */ + float2 ConvertToStereoUV(float2 uv, uint a_eyeIndex, uint a_invertY = 0) + { +#ifdef VR + // convert [0,1] to eye specific [0,.5] and [.5, 1] dependent on a_eyeIndex + uv.x = saturate(uv.x); + uv.x = (uv.x + (float)a_eyeIndex) / 2; + if (a_invertY) + uv.y = 1 - uv.y; +#endif + return uv; + } + + float3 ConvertToStereoUV(float3 uv, uint a_eyeIndex, uint a_invertY = 0) + { + uv.xy = ConvertToStereoUV(uv.xy, a_eyeIndex, a_invertY); + return uv; + } + + float4 ConvertToStereoUV(float4 uv, uint a_eyeIndex, uint a_invertY = 0) + { + uv.xy = ConvertToStereoUV(uv.xy, a_eyeIndex, a_invertY); + return uv; + } + + /** + Converts from eye specific uv to general uv [0,1]. + In VR, texture buffers include the left and right eye in the same buffer. + This means the x value [0, .5] represents the left eye, and the x value (.5, 1] are the right eye. + This returns the adjusted value + @param uv - eye specific uv coords [0,1]; if uv.x < 0.5, it's a left eye; otherwise right + @param a_eyeIndex The eyeIndex; 0 is left, 1 is right + @param a_invertY Whether to invert the Y direction + @returns uv with x coords adjusted to full range for either left or right eye + */ + float2 ConvertFromStereoUV(float2 uv, uint a_eyeIndex, uint a_invertY = 0) + { +#ifdef VR + // convert [0,.5] to [0, 1] and [.5, 1] to [0,1] + uv.x = 2 * uv.x - (float)a_eyeIndex; + if (a_invertY) + uv.y = 1 - uv.y; +#endif + return uv; + } + + float3 ConvertFromStereoUV(float3 uv, uint a_eyeIndex, uint a_invertY = 0) + { + uv.xy = ConvertFromStereoUV(uv.xy, a_eyeIndex, a_invertY); + return uv; + } + + float4 ConvertFromStereoUV(float4 uv, uint a_eyeIndex, uint a_invertY = 0) + { + uv.xy = ConvertFromStereoUV(uv.xy, a_eyeIndex, a_invertY); + return uv; + } + + /** + Gets the eyeIndex for Compute Shaders + @param texCoord Texcoord on the screen [0,1] + @returns eyeIndex (0 left, 1 right) + */ + uint GetEyeIndexFromTexCoord(float2 texCoord) + { +#ifdef VR + return (texCoord.x >= 0.5) ? 1 : 0; +#endif // VR + return 0; + } + + /** + * @brief Applies motion velocity to UV coordinates and determines if the resulting mono UV is out of screen bounds. + * @param uv Screen UV coordinates (stereo in VR, mono in SE) + * @param velocity Delta motion mapping + * @param isOutOfBounds Output flag indicating if the motion went out of bounds + * @return Newly displaced UV coordinate mapped back to correct space (stereo in VR, mono in SE). Clamped if necessary. + */ + float2 ApplyVelocityToUV(float2 uv, float2 velocity, out bool isOutOfBounds) + { + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + float2 prevUVmono = Stereo::ConvertFromStereoUV(uv, eyeIndex) + velocity; + float2 clampedMono = prevUVmono; + +#ifdef VR + // VR logic: mono.x < 0 is clamped to 0, not rejected. OOB fires for mono.x >= 1 or mono.y outside [0, 1] inclusive. + isOutOfBounds = (prevUVmono.x >= 1.0) || (prevUVmono.y <= 0.0) || (prevUVmono.y >= 1.0); + clampedMono.x = saturate(prevUVmono.x); +#else + // SE logic: inclusive boundaries on both sides. + isOutOfBounds = any(prevUVmono >= 1.0) || any(prevUVmono <= 0.0); +#endif + + return Stereo::ConvertToStereoUV(clampedMono, eyeIndex); + } + + /** + Converts to the eye specific screenposition [0,Resolution]. + In VR, texture buffers include the left and right eye in the same buffer. Flat only has a single camera for the entire width. + This means the x value [0, resx/2] represents the left eye, and the x value (resx/2, x] are the right eye. + This returns the adjusted value + @param screenPosition - Screenposition coords ([0,resx], [0,resy]) to be encoded for VR + @param a_eyeIndex The eyeIndex; 0 is left, 1 is right + @param a_resolution The resolution of the screen + @returns screenPosition with x coords adjusted for the VR texture buffer + */ + float2 ConvertToStereoSP(float2 screenPosition, uint a_eyeIndex, float2 a_resolution) + { + screenPosition.x /= a_resolution.x; + float2 stereoUV = ConvertToStereoUV(screenPosition, a_eyeIndex); + return stereoUV * a_resolution; + } + + float3 ConvertToStereoSP(float3 screenPosition, uint a_eyeIndex, float2 a_resolution) + { + float2 xy = screenPosition.xy / a_resolution; + xy = ConvertToStereoUV(xy, a_eyeIndex); + return float3(xy * a_resolution, screenPosition.z); + } + + float4 ConvertToStereoSP(float4 screenPosition, uint a_eyeIndex, float2 a_resolution) + { + float2 xy = screenPosition.xy / a_resolution; + xy = ConvertToStereoUV(xy, a_eyeIndex); + return float4(xy * a_resolution, screenPosition.zw); + } + + /** + * @brief Converts UV coordinates from the range [0, 1] to normalized screen space [-1, 1]. + * + * This function takes texture coordinates and transforms them into a normalized + * coordinate system centered at the origin. This is useful for various graphical + * calculations, especially in shaders that require symmetry around the center. + * + * @param uv The input UV coordinates in the range [0, 1]. + * @return float2 Normalized screen space coordinates in the range [-1, 1]. + */ + float2 ConvertUVToNormalizedScreenSpace(float2 uv) + { + float2 normalizedCoord; + normalizedCoord.x = 2.0 * (-0.5 + abs(2.0 * (uv.x - 0.5))); // Convert UV.x + normalizedCoord.y = 2.0 * uv.y - 1.0; // Convert UV.y + return normalizedCoord; + } + + /** + * @brief Returns the maximum absolute depth difference between a center depth and four neighbors. + * + * Used for depth-discontinuity edge detection in stereo sync passes. + * Works with both NDC depths (fixed absolute threshold) and linear view-space depths + * (relative threshold: divide result by max(center, 1.0)). + * + * @param[in] center Depth at the pixel being tested. + * @param[in] neighbors Depths at four neighboring pixels (e.g. ±1 or ±2 cross pattern). + * @return Maximum of |center - neighbor| across all four samples. + */ + float MaxDepthDiff(float center, float4 neighbors) + { + return max(max(abs(center - neighbors.x), abs(center - neighbors.y)), + max(abs(center - neighbors.z), abs(center - neighbors.w))); + } + + /** + * @brief Clamps a stereo UV coordinate to the eye-local X range of the packed stereo buffer. + * + * Prevents cross-neighbor UV samples from crossing the x=0.5 seam into the other eye's + * region of the side-by-side stereo texture. Y is not clamped; sampler address modes + * handle vertical out-of-bounds. + * + * @param[in] uv Stereo UV coordinate to clamp. + * @param[in] eyeIndex Eye index (0 = left [0, 0.5], 1 = right [0.5, 1]). + * @return UV with x restricted to eyeIndex's half of the stereo buffer. + */ + float2 ClampToEyeUV(float2 uv, uint eyeIndex) + { + uv.x = clamp(uv.x, eyeIndex == 0 ? 0.0f : 0.5f, eyeIndex == 0 ? 0.5f : 1.0f); + return uv; + } + + /** + * @brief Clamps a pixel coordinate to the eye-local X bounds of the packed stereo buffer. + * + * Prevents cross-neighbor pixel reads from crossing the half-width seam into the + * other eye's region of the side-by-side stereo texture. + * + * @param[in] px Pixel coordinate to clamp. + * @param[in] eyeIndex Eye index (0 = left, 1 = right). + * @param[in] frameDim Full stereo buffer dimensions (width covers both eyes). + * @return Clamped pixel coordinate, restricted to eyeIndex's half of the buffer. + */ + int2 ClampToEyeBounds(int2 px, uint eyeIndex, float2 frameDim) + { + int halfWidth = (int)((uint)frameDim.x >> 1); + px.x = clamp(px.x, eyeIndex == 0 ? 0 : halfWidth, eyeIndex == 0 ? (halfWidth - 1) : ((int)frameDim.x - 1)); + px.y = clamp(px.y, 0, (int)frameDim.y - 1); + return px; + } + +#if defined(PSHADER) || defined(FRAMEBUFFER) + // These functions require the framebuffer which is typically provided with the PSHADER + /** + Gets the eyeIndex for PSHADER + @returns eyeIndex (0 left, 1 right) + */ + uint GetEyeIndexPS(float4 position, float4 offset = 0.0.xxxx) + { +# if !defined(VR) + uint eyeIndex = 0; +# else + float4 stereoUV; + stereoUV.xy = position.xy * offset.xy + offset.zw; + stereoUV.x = FrameBuffer::DynamicResolutionParams2.x * stereoUV.x; + stereoUV.x = (stereoUV.x >= 0.5); + uint eyeIndex = (uint)(((int)((uint)StereoEnabled)) * (int)stereoUV.x); +# endif + return eyeIndex; + } + + /** + * @brief Checks if the color is non zero by testing if the color is greater than 0 by epsilon. + * + * This function check is a color is non black. It uses a small epsilon value to allow for + * floating point imprecision. + * + * For screen-space reflection (SSR), this acts as a mask and checks for an invalid reflection by + * checking if the reflection color is essentially black (close to zero). + * + * @param[in] ssrColor The color to check. + * @param[in] epsilon Small tolerance value used to determine if the color is close to zero. + * @return True if color is non zero, otherwise false. + */ + bool IsNonZeroColor(float4 ssrColor, float epsilon = 0.001) + { + return dot(ssrColor.xyz, ssrColor.xyz) > epsilon * epsilon; + } + +# ifdef VR + /** + * @brief Converts mono UV coordinates from one eye to the corresponding mono UV coordinates of the other eye. + * + * This function is used to transition UV coordinates from one eye's perspective to the other eye in a stereo rendering setup. + * It operates by converting the mono UV to clip space, transforming it into world space, and then reprojecting it + * into the other eye's clip space before converting back to UV coordinates. It supports dynamic resolution adjustments + * and applies eye offset adjustments for correct stereo separation. + * + * The function considers the aspect of VR by modifying the NDC to view space conversion based on the stereo setup, + * ensuring accurate rendering across both eyes. + * + * @param[in] monoUV The UV coordinates and depth value (Z component) for the current eye, in the range [0,1]. + * @param[in] eyeIndex Index of the source/current eye (0 for left, 1 for right). + * @param[in] dynamicres Optional flag indicating whether dynamic resolution is applied. Default is false. + * @return UV coordinates adjusted to the other eye, with depth. + */ + float3 ConvertMonoUVToOtherEye(float3 monoUV, uint eyeIndex, bool dynamicres = false) + { + // Convert from dynamic res to true UV space if necessary + if (dynamicres) + monoUV.xy *= FrameBuffer::DynamicResolutionParams2.xy; + + // Convert UV to Clip Space + float4 clipPos = float4(monoUV.xy * float2(2, -2) - float2(1, -1), monoUV.z, 1); + + // Convert Clip Space to World Space for the current eye + float4 worldPos = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], clipPos); + worldPos /= worldPos.w; + + // Apply eye offset adjustment in world space + worldPos.xyz += FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[1 - eyeIndex].xyz; + + // Convert World Space to Clip Space for the other eye + float4 clipPosOtherEye = mul(FrameBuffer::CameraViewProj[1 - eyeIndex], worldPos); + clipPosOtherEye /= clipPosOtherEye.w; + + // Convert Clip Space to UV (Y is flipped: clip +1 = top, UV 0 = top) + float3 monoUVOtherEye = float3(clipPosOtherEye.xy * float2(0.5f, -0.5f) + 0.5f, clipPosOtherEye.z); + + // Convert back to dynamic res space if necessary + if (dynamicres) + monoUVOtherEye.xy *= FrameBuffer::DynamicResolutionParams1.xy; + + return monoUVOtherEye; + } +# endif // VR + + /** + * @brief Resolves a mono UV to the eye that can see it, crossing to the other eye if needed. + * + * When a screen-space ray or sample position leaves the current eye's screen bounds, + * this function tries to find the corresponding location in the other eye via + * ConvertMonoUVToOtherEye. On flat (non-VR) this is a no-op: sampleUV and + * sampleEyeIndex are set to the input values unchanged. + * + * Based on concepts from https://cuteloong.github.io/publications/scssr24/ + * Wu, X., Xu, Y., & Wang, L. (2024). Stereo-consistent Screen Space Reflection. Computer Graphics Forum, 43(4). + * + * @param[in] monoUV Mono UV coordinates with depth in Z, [0-1]. Must not be dynamic resolution adjusted. + * @param[in] eyeIndex Index of the originating eye (0 or 1). + * @param[out] sampleUV Mono UV that should be used for sampling (may be in the other eye). + * @param[out] sampleEyeIndex Eye index that owns sampleUV. + */ + void ResolveMonoUVForEye(float3 monoUV, uint eyeIndex, out float2 sampleUV, out uint sampleEyeIndex) + { + sampleUV = monoUV.xy; + sampleEyeIndex = eyeIndex; +# ifdef VR + if (FrameBuffer::IsOutsideFrame(monoUV.xy, false)) { + float3 otherEyeUV = ConvertMonoUVToOtherEye(monoUV, eyeIndex); + if (!FrameBuffer::IsOutsideFrame(otherEyeUV.xy, false)) { + sampleUV = otherEyeUV.xy; + sampleEyeIndex = 1 - eyeIndex; + } + } +# endif + } + +# ifdef VR + /** + * @brief Adjusts UV coordinates for VR stereo rendering when transitioning between eyes or handling boundary conditions. + * + * This function is used in raymarching to check the next UV coordinate. It checks if the current UV coordinates are outside + * the frame. If so, it transitions the UV coordinates to the other eye and adjusts them if they are within the frame of the other eye. + * If the UV coordinates are outside the frame of both eyes, it returns the adjusted UV coordinates for the current eye. + * + * The function ensures that the UV coordinates are correctly adjusted for stereo rendering, taking into account boundary conditions + * and preserving accurate reflections. + * Based on concepts from https://cuteloong.github.io/publications/scssr24/ + * Wu, X., Xu, Y., & Wang, L. (2024). Stereo-consistent Screen Space Reflection. Computer Graphics Forum, 43(4). + * + * We do not have a backface depth so we may be ray marching even though the ray is in an object. + + * @param[in] monoUV Current UV coordinates with depth information, [0-1]. Must not be dynamic resolution adjusted. + * @param[in] eyeIndex Index of the current eye (0 or 1). + * @param[out] fromOtherEye Boolean indicating if the result UV coordinates are from the other eye. + * + * @return Adjusted stereo UV coordinates for rendering, [0-1]. Must be dynamic resolution adjusted later. + */ + float3 ConvertStereoRayMarchUV(float3 monoUV, uint eyeIndex, out bool fromOtherEye) + { + float2 resolvedUV; + uint resolvedEye; + ResolveMonoUVForEye(monoUV, eyeIndex, resolvedUV, resolvedEye); + fromOtherEye = (resolvedEye != eyeIndex); + return ConvertToStereoUV(float3(resolvedUV, monoUV.z), resolvedEye); + } + + /** + * @brief Converts stereo UV coordinates from one eye to the corresponding stereo UV coordinates of the other eye. + * + * This function is used to transition UV coordinates from one eye's perspective to the other eye in a stereo rendering setup. + * It works by converting the stereo UV to mono UV, then to clip space, transforming it into view space, and then reprojecting it into the other eye's + * clip space before converting back to stereo UV coordinates. It also supports dynamic resolution. + * + * @param[in] stereoUV The UV coordinates and depth value (Z component) for the current eye, in the range [0,1]. + * @param[in] eyeIndex Index of the current eye (0 or 1). + * @param[in] dynamicres Optional flag indicating whether dynamic resolution is applied. Default is false. + * @return UV coordinates adjusted to the other eye, with depth. + */ + float3 ConvertStereoUVToOtherEyeStereoUV(float3 stereoUV, uint eyeIndex, bool dynamicres = false) + { + // Convert from dynamic res to true UV space + if (dynamicres) + stereoUV.xy *= FrameBuffer::DynamicResolutionParams2.xy; + + stereoUV.xy = ConvertFromStereoUV(stereoUV.xy, eyeIndex); + stereoUV.xyz = ConvertMonoUVToOtherEye(stereoUV.xyz, eyeIndex); + stereoUV.xy = ConvertToStereoUV(stereoUV.xy, 1 - eyeIndex); + + // Convert back to dynamic res space if necessary + if (dynamicres) + stereoUV.xy *= FrameBuffer::DynamicResolutionParams1.xy; + return stereoUV; + } + + /** + * @brief Returns a smooth fade factor for UVs near the edge of the frame. + * + * This helps avoid abrupt transitions when one eye's SSGI is out of frame or occluded. + * Fade width is tunable; 0.02 is 2% of the frame. + */ + float IsOutsideFrameFade(float2 uv, bool dynamicres = false) + { + float2 max = dynamicres ? FrameBuffer::DynamicResolutionParams1.xy : float2(1, 1); + float2 min = float2(0, 0); + float fadeWidth = 0.02; + float edgeFade = 1.0; + edgeFade *= smoothstep(min.x, min.x + fadeWidth, uv.x); + edgeFade *= smoothstep(max.x, max.x - fadeWidth, uv.x); + edgeFade *= smoothstep(min.y, min.y + fadeWidth, uv.y); + edgeFade *= smoothstep(max.y, max.y - fadeWidth, uv.y); + return edgeFade; + } + + /** + * @brief Blends color data from two eyes based on their UV coordinates and validity. + * + * This function checks the validity of the colors based on their UV coordinates and + * alpha values. It blends the colors while ensuring proper handling of transparency. + * If one eye sees the first person model (depth < VR_FP_Z) and the other sees world geometry (depth > VR_FP_Z), + * the first person model's color is dropped from the blend to avoid outlines. + * + * @param uv1 UV coordinates for the first eye. + * @param color1 Color from the first eye. + * @param uv2 UV coordinates for the second eye. + * @param color2 Color from the second eye. + * @param dynamicres Whether the uvs have dynamic resolution applied + * @return Blended color, including the maximum alpha from both inputs. + */ + float4 BlendEyeColors( + float3 uv1, + float4 color1, + float3 uv2, + float4 color2, + bool dynamicres = false) + { + // Use smooth fade at edge for each eye + float fade1 = IsOutsideFrameFade(uv1.xy, dynamicres); + float fade2 = IsOutsideFrameFade(uv2.xy, dynamicres); + + // Stereo-consistent edge fade: use maximum fade so either eye can keep color if in bounds + float edgeFade = max(fade1, fade2); + + // Occlusion-aware confidence based on depth difference + float depthDiff = abs(uv1.z - uv2.z); + float confidence = 1.0 - smoothstep(0.01, 0.05, depthDiff); + + // Soft first person model mask: fade out FP model near threshold + float fp_fade1 = 1.0 - smoothstep(VR_FP_Z - 1.0, VR_FP_Z + 1.0, uv1.z); // fades from 1 to 0 as depth crosses VR_FP_Z + float fp_fade2 = 1.0 - smoothstep(VR_FP_Z - 1.0, VR_FP_Z + 1.0, uv2.z); + + // If one eye is world and the other is FP, fade out FP smoothly + bool eye1_is_fp = uv1.z < VR_FP_Z; + bool eye2_is_fp = uv2.z < VR_FP_Z; + bool eyes_disagree = eye1_is_fp != eye2_is_fp; + if (eyes_disagree) { + if (eye1_is_fp) + fade1 *= fp_fade1; + if (eye2_is_fp) + fade2 *= fp_fade2; + } + + fade1 *= confidence * edgeFade; + fade2 *= confidence * edgeFade; + + float totalFade = fade1 + fade2 + 1e-5; + float4 blendedColor = (color1 * fade1 + color2 * fade2) / totalFade; + blendedColor.a = max(color1.a * fade1, color2.a * fade2); + return blendedColor; + } + + float4 BlendEyeColors(float2 uv1, float4 color1, float2 uv2, float4 color2, bool dynamicres = false) + { + return BlendEyeColors(float3(uv1, 0), color1, float3(uv2, 0), color2, dynamicres); + } + + /** + * @brief Result of a stereo bilateral reprojection: other-eye pixel coords and blend weight. + */ + struct StereoBilateralResult + { + float2 otherStereoUV; ///< Stereo UV in the other eye + int2 otherPx; ///< Pixel coordinate in the other eye + float blendWeight; ///< [0, maxBlend] bilateral blend weight + bool valid; ///< True if reprojection succeeded + bool backCheckPassed; ///< True if round-trip reprojection validated + }; + + /** + * @brief Reprojects a pixel to the other eye and computes pixel coordinates. + * + * First stage of the stereo bilateral filter from Shi, Billeter, Eisemann 2022. + * Returns the other-eye pixel location; the caller must sample depth at that + * location and call FinalizeStereoBlend to complete the bilateral weight. + * + * @param[in] stereoUV Stereo UV of the source pixel [0,1] + * @param[in] depth Depth at the source pixel + * @param[in] eyeIndex Eye index of the source pixel (0 or 1) + * @param[in] frameDim Dimensions of the buffer (for pixel coord conversion) + * @return StereoBilateralResult with valid=false if reprojection is out of bounds. + */ + StereoBilateralResult ReprojectToOtherEye( + float2 stereoUV, + float depth, + uint eyeIndex, + float2 frameDim) + { + StereoBilateralResult result; + result.otherStereoUV = 0; + result.otherPx = int2(0, 0); + result.blendWeight = 0; + result.valid = false; + result.backCheckPassed = false; + + uint otherEyeIndex = 1 - eyeIndex; + + float2 monoUV = ConvertFromStereoUV(stereoUV, eyeIndex); + float3 otherEyeUV = ConvertMonoUVToOtherEye(float3(monoUV, depth), eyeIndex); + + if (FrameBuffer::IsOutsideFrame(otherEyeUV.xy, false)) + return result; + + result.otherStereoUV = ConvertToStereoUV(otherEyeUV.xy, otherEyeIndex); + result.otherPx = clamp(int2(result.otherStereoUV * frameDim), int2(0, 0), int2(frameDim) - 1); + result.valid = true; + return result; + } + + /** + * @brief Computes bilateral blend weight with depth comparison and back-check. + * + * Second stage of the stereo bilateral filter from Shi, Billeter, Eisemann 2022. + * Compares the sampled depth at the other eye's pixel against the expected depth, + * and performs the back-check (round-trip reprojection validation). + * + * @param[in,out] result Result from ReprojectToOtherEye; blendWeight and backCheckPassed are filled in. + * @param[in] stereoUV Stereo UV of the source pixel (same as passed to ReprojectToOtherEye) + * @param[in] depth Depth at the source pixel + * @param[in] otherEyeDepth Actual depth sampled at the other eye's pixel + * @param[in] eyeIndex Eye index of the source pixel + * @param[in] frameDim Dimensions of the buffer + * @param[in] depthSigma Gaussian sigma for bilateral depth weight + * @param[in] maxBlend Maximum blend factor + * @param[in] backCheckThreshold Max pixel distance for back-check (0 to disable). Default 8.0. + */ + void FinalizeStereoBlend( + inout StereoBilateralResult result, + float2 stereoUV, + float depth, + float otherEyeDepth, + uint eyeIndex, + float2 frameDim, + float depthSigma, + float maxBlend, + float backCheckThreshold = 8.0) + { + // Bilateral weight: compare sampled depth at other eye against source depth + float depthDiff = abs(depth - otherEyeDepth); + float depthWeight = exp(-depthDiff * depthDiff / (depthSigma * depthSigma + 1e-8)); + + // Back-check: reproject Q (in eye B) back to eye A and verify round-trip. + // Two VP matrix multiplications accumulate float32 error (~3-5px at medium range), + // so the threshold must be generous enough to pass valid surfaces while catching + // true occlusion discontinuities (which produce errors of tens to hundreds of pixels). + uint otherEyeIndex = 1 - eyeIndex; + result.backCheckPassed = true; + if (backCheckThreshold > 0) { + float2 otherMonoUV = ConvertFromStereoUV(result.otherStereoUV, otherEyeIndex); + float3 roundTripUV = ConvertMonoUVToOtherEye(float3(otherMonoUV, otherEyeDepth), otherEyeIndex); + float2 roundTripStereoUV = ConvertToStereoUV(roundTripUV.xy, eyeIndex); + float2 pixelDist = abs(roundTripStereoUV * frameDim - (stereoUV * frameDim)); + // Use max component so a large error in either axis triggers the check + result.backCheckPassed = max(pixelDist.x, pixelDist.y) < backCheckThreshold; + if (!result.backCheckPassed) + depthWeight *= 0.1; // Heavily penalize but don't fully reject + } + + result.blendWeight = depthWeight * maxBlend; + } +# endif // VR +#endif // PSHADER + +#ifdef VSHADER + struct VR_OUTPUT + { + float4 VRPosition; + float ClipDistance; + float CullDistance; + }; + + /** + Gets the eyeIndex for VSHADER + @returns eyeIndex (0 left, 1 right) + */ + uint GetEyeIndexVS(uint instanceID = 0) + { +# ifdef VR + return StereoEnabled * (instanceID & 1); +# endif // VR + return 0; + } + + /** + Gets VR Output + @param clipPos clipPosition. Typically the VSHADER position at SV_POSITION0 + @param a_eyeIndex The eyeIndex; 0 is left, 1 is right + @returns VR_OUTPUT with VR values + */ + VR_OUTPUT GetVRVSOutput(float4 clipPos, uint a_eyeIndex = 0) + { + VR_OUTPUT vsout = { + 0.0.xxxx, // VRPosition + 0.0f, // ClipDistance + 0.0f // CullDistance + }; + +# ifdef VR + bool isStereoEnabled = (StereoEnabled != 0); + float2 clipEdges; + + if (isStereoEnabled) { + clipEdges.x = dot(clipPos, EyeClipEdge[a_eyeIndex]); + clipEdges.y = clipEdges.x; // Both use the same calculation + } else { + clipEdges = float2(1.0f, 1.0f); + } + + float stereoAdjustment = 2.0f - StereoEnabled; + float eyeOffset = dot(EyeOffsetScale, Math::IdentityMatrix[a_eyeIndex].xy); + + float xPositionOffset = eyeOffset * clipPos.w * (isStereoEnabled ? 1.0f : 0.0f); + float xPositionBase = stereoAdjustment * clipPos.x; + + vsout.VRPosition.x = xPositionBase * 0.5f + xPositionOffset; + vsout.VRPosition.y = clipPos.y; + vsout.VRPosition.z = clipPos.z; + vsout.VRPosition.w = clipPos.w; + + vsout.ClipDistance = clipEdges.y; + vsout.CullDistance = clipEdges.x; +# endif // VR + return vsout; + } +#endif + +} +#endif //__VR_DEPENDENCY_HLSL__ \ No newline at end of file diff --git a/package/Shaders/DeferredCompositeCS.hlsl b/package/Shaders/DeferredCompositeCS.hlsl index 9075bfc0ba..cadbf90424 100644 --- a/package/Shaders/DeferredCompositeCS.hlsl +++ b/package/Shaders/DeferredCompositeCS.hlsl @@ -7,6 +7,8 @@ #include "Common/Shading.hlsli" #include "Common/SharedData.hlsli" #include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" +#include "Common/VR.hlsli" + Texture2D SpecularTexture : register(t0); Texture2D AlbedoTexture : register(t1); Texture2D NormalRoughnessTexture : register(t2); @@ -25,6 +27,11 @@ Texture2D DepthTexture : register(t4); Texture2D DepthTexture : register(t4); #endif +#if defined(VR_STEREO_OPT) +# include "VRStereoOptimizations/modes.hlsli" +Texture2D StereoOptModeTexture : register(t16); +#endif + #if defined(DYNAMIC_CUBEMAPS) Texture2D ReflectanceTexture : register(t5); TextureCube EnvTexture : register(t6); @@ -94,6 +101,19 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float2 uv = float2(dispatchID.xy + 0.5) * SharedData::BufferDim.zw; uv *= FrameBuffer::DynamicResolutionParams2.xy; // adjust for dynamic res + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + +#if defined(VR_STEREO_OPT) + if (eyeIndex == 1) { + uint mode = StereoOptModeTexture[uint2(dispatchID.xy)] & 0x0F; + if (mode == MODE_MAIN) { // stencil-culled in Eye 1, filled by ReprojectionCS + return; + } + } +#endif + + uv = Stereo::ConvertFromStereoUV(uv, eyeIndex); + float3 normalGlossiness = NormalRoughnessTexture[dispatchID.xy]; float3 normalVS = GBuffer::DecodeNormal(normalGlossiness.xy); @@ -103,16 +123,16 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float depth = DepthTexture[dispatchID.xy]; float4 positionWS = float4(2 * float2(uv.x, -uv.y + 1) - 1, depth, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; if (depth == 1.0) - MotionVectorsRW[dispatchID.xy] = MotionBlur::GetSSMotionVector(positionWS, positionWS); // Apply sky motion vectors + MotionVectorsRW[dispatchID.xy] = MotionBlur::GetSSMotionVector(positionWS, positionWS, eyeIndex); // Apply sky motion vectors float glossiness = normalGlossiness.z; float3 linDiffuseColor = Color::IrradianceToLinear(diffuseColor); - float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse, float4(normalVS, 0)).xyz); + float3 normalWS = normalize(mul(FrameBuffer::CameraViewInverse[eyeIndex], float4(normalVS, 0)).xyz); #if defined(SSGI) @@ -135,7 +155,11 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float3 vanillaDALC = Color::Ambient(max(0, SharedData::GetAmbient(normalWS))); # if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMS = positionWS.xyz; +# endif sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, normalWS); float skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, normalWS); directionalAmbientColor = ImageBasedLighting::GetDiffuseIBLOccluded(vanillaDALC, -normalWS, skylightingDiffuse) * albedo; @@ -200,7 +224,11 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, float directionalAmbientColorSpecular = Color::RGBToLuminance(Color::Ambient(max(0, SharedData::GetAmbient(R)))) * Color::ReflectionNormalisationScale; # if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMS = positionWS.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMS = positionWS.xyz; +# endif sh2 skylightingSH = Skylighting::Sample(positionMS.xyz, R); float skylightingSpecular = Skylighting::EvaluateSpecular(skylightingSH, specularLobe); @@ -294,6 +322,10 @@ void SampleSSGISpecular(uint2 pixCoord, sh2 lobe, inout float ao, out float3 il, #if defined(DEBUG) +# if defined(VR) + uv.x += (eyeIndex ? 0.1 : -0.1); +# endif // VR + if (uv.x < 0.5 && uv.y < 0.5) { color = color; } else if (uv.x < 0.5) { diff --git a/package/Shaders/DistantTree.hlsl b/package/Shaders/DistantTree.hlsl index 70708e733a..3e510142a2 100644 --- a/package/Shaders/DistantTree.hlsl +++ b/package/Shaders/DistantTree.hlsl @@ -5,6 +5,8 @@ #include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" + #if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) # undef IBL #endif @@ -17,6 +19,9 @@ struct VS_INPUT float4 InstanceData2: TEXCOORD5; float4 InstanceData3: TEXCOORD6; float4 InstanceData4: TEXCOORD7; +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -32,6 +37,11 @@ struct VS_OUTPUT #endif // RENDER_DEPTH float4 ViewPosition: POSITION3; +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 + uint EyeIndex: EYEIDX0; +#endif // VR }; #ifdef VSHADER @@ -42,14 +52,25 @@ cbuffer PerTechnique : register(b0) cbuffer PerGeometry : register(b2) { - row_major float4x4 WorldViewProj : packoffset(c0); - row_major float4x4 World : packoffset(c4); - row_major float4x4 PreviousWorld : packoffset(c8); +# if !defined(VR) + row_major float4x4 WorldViewProj[1] : packoffset(c0); + row_major float4x4 World[1] : packoffset(c4); + row_major float4x4 PreviousWorld[1] : packoffset(c8); +# else + row_major float4x4 WorldViewProj[2] : packoffset(c0); + row_major float4x4 World[2] : packoffset(c8); + row_major float4x4 PreviousWorld[2] : packoffset(c16); +# endif // !VR }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout = (VS_OUTPUT)0; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif // VR + ); float3 scaledModelPosition = input.InstanceData1.www * input.Position.xyz; float3 adjustedModelPosition = 0.0.xxx; @@ -57,20 +78,28 @@ VS_OUTPUT main(VS_INPUT input) adjustedModelPosition.y = dot(input.InstanceData2.yx, scaledModelPosition.xy); adjustedModelPosition.z = scaledModelPosition.z; float4 finalModelPosition = float4(input.InstanceData1.xyz + adjustedModelPosition.xyz, 1.0); - float4 viewPosition = mul(WorldViewProj, finalModelPosition); + float4 viewPosition = mul(WorldViewProj[eyeIndex], finalModelPosition); # ifdef RENDER_DEPTH vsout.Depth.xy = viewPosition.zw; vsout.Depth.zw = input.InstanceData2.zw; # else - vsout.WorldPosition = mul(World, finalModelPosition); - vsout.PreviousWorldPosition = mul(PreviousWorld, finalModelPosition); + vsout.WorldPosition = mul(World[eyeIndex], finalModelPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], finalModelPosition); vsout.ViewPosition = viewPosition; # endif // RENDER_DEPTH vsout.Position = viewPosition; vsout.TexCoord = float3(input.TexCoord0.xy, FogParam.z); +# ifdef VR + vsout.EyeIndex = eyeIndex; + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); + vsout.Position = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR + return vsout; } #endif // VSHADER @@ -96,10 +125,12 @@ SamplerState SampDiffuse : register(s0); Texture2D TexDiffuse : register(t0); +# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif // !VR cbuffer PerFrame : register(b12) { @@ -151,10 +182,10 @@ const static float DepthOffsets[16] = { # include "Common/ShadowSampling.hlsli" # if defined(EXP_HEIGHT_FOG) -void ApplyReflectionExponentialHeightFog(inout float3 color, float3 positionWS, float4 screenPosition) +void ApplyReflectionExponentialHeightFog(inout float3 color, float3 positionWS, float4 screenPosition, uint eyeIndex) { float3 fogColor = Color::Fog(AmbientColor.xyz); - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(positionWS, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(screenPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, screenPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(positionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(screenPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, screenPosition.z, 1)); color = lerp(color, exponentialHeightFog.xyz, exponentialHeightFog.w); } # endif @@ -163,6 +194,11 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; +# if !defined(VR) + uint eyeIndex = 0; +# else + uint eyeIndex = input.EyeIndex; +# endif // !VR # if defined(EXP_HEIGHT_FOG) const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; # endif @@ -195,25 +231,25 @@ PS_OUTPUT main(PS_INPUT input) } # if defined(DEFERRED) - float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); float dirShadow = 1; # if defined(SCREEN_SPACE_SHADOWS) - dirShadow = lerp(1.0, ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise), 0.8); + dirShadow = lerp(1.0, ScreenSpaceShadows::GetScreenSpaceShadow(input.Position.xyz, screenUV, screenNoise, eyeIndex), 0.8); # endif if (dirShadow != 0.0) - dirShadow *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirShadow *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); float llDirLightMult = (SharedData::linearLightingSettings.enableLinearLighting && !SharedData::linearLightingSettings.isDirLightLinear) ? SharedData::linearLightingSettings.dirLightMult : 1.0f; float3 diffuseColor = Color::DirectionalLight(SharedData::DirLightColor.xyz / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * dirShadow * 0.5 * llDirLightMult * Color::VanillaNormalization(); # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif @@ -234,26 +270,26 @@ PS_OUTPUT main(PS_INPUT input) # if defined(EXP_HEIGHT_FOG) if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { - ApplyReflectionExponentialHeightFog(psout.Diffuse.xyz, input.WorldPosition.xyz, input.Position); + ApplyReflectionExponentialHeightFog(psout.Diffuse.xyz, input.WorldPosition.xyz, input.Position, eyeIndex); } # endif - psout.MotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + psout.MotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); - psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false)); + psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false, eyeIndex)); psout.Normal.zw = 0; psout.Albedo = float4(baseColor.xyz, 1); psout.Masks = float4(0, 0, 1, 0); # else - float dirShadow = ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + float dirShadow = ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); float llDirLightMult = (SharedData::linearLightingSettings.enableLinearLighting && !SharedData::linearLightingSettings.isDirLightLinear) ? SharedData::linearLightingSettings.dirLightMult : 1.0f; float3 diffuseColor = Color::DirectionalLight(SharedData::DirLightColor.xyz / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * dirShadow * 0.5 * llDirLightMult * Color::VanillaNormalization(); # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + diffuseColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif @@ -272,7 +308,7 @@ PS_OUTPUT main(PS_INPUT input) float3 color = diffuseColor * baseColor.xyz; # if defined(EXP_HEIGHT_FOG) if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { - ApplyReflectionExponentialHeightFog(color, input.WorldPosition.xyz, input.Position); + ApplyReflectionExponentialHeightFog(color, input.WorldPosition.xyz, input.Position, eyeIndex); } # endif psout.Diffuse = float4(color, 1.0); diff --git a/package/Shaders/Effect.hlsl b/package/Shaders/Effect.hlsl index 28d7630589..8d19642f03 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -7,6 +7,8 @@ #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" +#include "Common/VR.hlsli" + #define EFFECT #if !defined(DYNAMIC_CUBEMAPS) && defined(IBL) @@ -37,6 +39,9 @@ struct VS_INPUT float4 BoneWeights: BLENDWEIGHT0; float4 BoneIndices: BLENDINDICES0; #endif +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -82,19 +87,35 @@ struct VS_OUTPUT float3 ScreenSpaceNormal: TEXCOORD7; # endif #endif +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 + uint EyeIndex: EYEIDX0; +#endif // VR }; #ifdef VSHADER cbuffer VS_PerFrame : register(b12) { - row_major float4x4 ScreenProj : packoffset(c0); - row_major float4x4 ViewProj : packoffset(c8); -# if defined(SKINNED) - float3 BonesPivot : packoffset(c40); -# if defined(MOTIONVECTORS_NORMALS) - float3 PreviousBonesPivot : packoffset(c41); -# endif // MOTIONVECTORS_NORMALS -# endif // SKINNED +# if !defined(VR) + row_major float4x4 ScreenProj[1] : packoffset(c0); + row_major float4x4 ViewProj[1] : packoffset(c8); +# if defined(SKINNED) + float3 BonesPivot[1] : packoffset(c40); +# if defined(MOTIONVECTORS_NORMALS) + float3 PreviousBonesPivot[1] : packoffset(c41); +# endif // MOTIONVECTORS_NORMALS +# endif // SKINNED +# else + row_major float4x4 ScreenProj[2] : packoffset(c0); + row_major float4x4 ViewProj[2] : packoffset(c16); +# if defined(SKINNED) + float3 BonesPivot[2] : packoffset(c80); +# if defined(MOTIONVECTORS_NORMALS) + float3 PreviousBonesPivot[2] : packoffset(c82); +# endif // MOTIONVECTORS_NORMALS +# endif // SKINNED +# endif // VR }; cbuffer PerTechnique : register(b0) @@ -113,12 +134,21 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { - row_major float3x4 World : packoffset(c0); - row_major float3x4 PreviousWorld : packoffset(c3); +# if !defined(VR) + row_major float3x4 World[1] : packoffset(c0); + row_major float3x4 PreviousWorld[1] : packoffset(c3); float4 MatProj[3] : packoffset(c6); - float4 EyePosition : packoffset(c12); - float4 PosAdjust : packoffset(c13); + float4 EyePosition[1] : packoffset(c12); + float4 PosAdjust[1] : packoffset(c13); float4 TexcoordOffsetMembrane : packoffset(c14); +# else + row_major float3x4 World[2] : packoffset(c0); + row_major float3x4 PreviousWorld[2] : packoffset(c6); + float4 MatProj[3] : packoffset(c12); + float4 EyePosition[2] : packoffset(c21); + float4 PosAdjust[2] : packoffset(c23); + float4 TexcoordOffsetMembrane : packoffset(c25); +# endif // VR } cbuffer IndexedTexcoordBuffer : register(b11) @@ -160,42 +190,47 @@ float GetProjectedU(float3 worldPosition, float4 texCoordOffset) return abs(0.318309158 * projUvTmp4) * texCoordOffset.w + texCoordOffset.y; } -float GetProjectedV(float3 worldPosition) +float GetProjectedV(float3 worldPosition, uint a_eyeIndex = 0) { - return (-PosAdjust.x + (PosAdjust.z + worldPosition.z)) / PosAdjust.y; + return (-PosAdjust[a_eyeIndex].x + (PosAdjust[a_eyeIndex].z + worldPosition.z)) / PosAdjust[a_eyeIndex].y; } # endif VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif // VR + ); precise float4 inputPosition = float4(input.Position.xyz, 1.0); - precise row_major float4x4 world4x4 = float4x4(World[0], World[1], World[2], float4(0, 0, 0, 1)); + precise row_major float4x4 world4x4 = float4x4(World[eyeIndex][0], World[eyeIndex][1], World[eyeIndex][2], float4(0, 0, 0, 1)); precise float3x3 world3x3 = - transpose(float3x3(transpose(World)[0], transpose(World)[1], transpose(World)[2])); + transpose(float3x3(transpose(World[eyeIndex])[0], transpose(World[eyeIndex])[1], transpose(World[eyeIndex])[2])); # if defined(SKY_OBJECT) - float4x4 viewProj = float4x4(ViewProj[0], ViewProj[1], ViewProj[3], ViewProj[3]); + float4x4 viewProj = float4x4(ViewProj[eyeIndex][0], ViewProj[eyeIndex][1], ViewProj[eyeIndex][3], ViewProj[eyeIndex][3]); # else - row_major float4x4 viewProj = ViewProj; + row_major float4x4 viewProj = ViewProj[eyeIndex]; # endif # if defined(SKINNED) precise int4 actualIndices = 765.01.xxxx * input.BoneIndices.xyzw; # if defined(MOTIONVECTORS_NORMALS) float3x4 previousBoneTransformMatrix = - Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot, input.BoneWeights); + Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot[eyeIndex], input.BoneWeights); precise float4 previousWorldPosition = float4(mul(inputPosition, transpose(previousBoneTransformMatrix)), 1); # endif float3x4 boneTransformMatrix = - Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot, input.BoneWeights); + Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot[eyeIndex], input.BoneWeights); precise float4 worldPosition = float4(mul(inputPosition, transpose(boneTransformMatrix)), 1); float4 viewPos = mul(viewProj, worldPosition); # else - precise float4 worldPosition = float4(mul(World, inputPosition), 1); - precise float4 previousWorldPosition = float4(mul(PreviousWorld, inputPosition), 1); + precise float4 worldPosition = float4(mul(World[eyeIndex], inputPosition), 1); + precise float4 previousWorldPosition = float4(mul(PreviousWorld[eyeIndex], inputPosition), 1); precise row_major float4x4 modelView = mul(viewProj, world4x4); float4 viewPos = mul(modelView, inputPosition); # endif @@ -264,7 +299,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(NORMALS) && !defined(MEMBRANE) texCoord.y = dot(MatProj[1].xyz, inputPosition.xyz); # else - texCoord.y = GetProjectedV(worldPosition.xyz); + texCoord.y = GetProjectedV(worldPosition.xyz, eyeIndex); # endif # else # if defined(TEXTURE) @@ -297,7 +332,7 @@ VS_OUTPUT main(VS_INPUT input) float3 eyePosition = 0.0.xxx; # if defined(MEMBRANE) && defined(TEXTURE) && !defined(SKINNED) - eyePosition = EyePosition.xyz; + eyePosition = EyePosition[eyeIndex].xyz; # endif float3 viewPosition = inputPosition.xyz; @@ -340,7 +375,7 @@ VS_OUTPUT main(VS_INPUT input) # elif defined(FALLOFF) || (defined(SKINNED) && defined(MEMBRANE)) float3 screenSpaceNormal = worldNormal; # else - float4x4 modelScreen = mul(ScreenProj, world4x4); + float4x4 modelScreen = mul(ScreenProj[eyeIndex], world4x4); float3 screenSpaceNormal = normalize(mul(modelScreen, float4(normal, 0))).xyz; # endif @@ -362,6 +397,13 @@ VS_OUTPUT main(VS_INPUT input) vsout.PreviousWorldPosition = previousWorldPosition; # endif +# ifdef VR + vsout.EyeIndex = eyeIndex; + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); + vsout.Position = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR return vsout; } #endif @@ -409,10 +451,12 @@ struct PS_OUTPUT #ifdef PSHADER +# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif // !VR cbuffer PerTechnique : register(b0) { @@ -430,6 +474,7 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { +# if !defined(VR) float4 PLightPositionX[1] : packoffset(c0); float4 PLightPositionY[1] : packoffset(c1); float4 PLightPositionZ[1] : packoffset(c2); @@ -442,6 +487,20 @@ cbuffer PerGeometry : register(b2) float4 AlphaTestRef : packoffset(c9); float4 MembraneRimColor : packoffset(c10); float4 MembraneVars : packoffset(c11); +# else + float4 PLightPositionX[2] : packoffset(c0); + float4 PLightPositionY[2] : packoffset(c2); + float4 PLightPositionZ[2] : packoffset(c4); + float4 PLightingRadiusInverseSquared : packoffset(c6); + float4 PLightColorR : packoffset(c7); + float4 PLightColorG : packoffset(c8); + float4 PLightColorB : packoffset(c9); + float4 DLightColor : packoffset(c10); + float4 PropertyColor : packoffset(c11); // VR should be 11; this could start earlier though + float4 AlphaTestRef : packoffset(c12); + float4 MembraneRimColor : packoffset(c13); + float4 MembraneVars : packoffset(c14); +# endif }; # define LinearSampler SampBaseSampler @@ -507,7 +566,7 @@ void ExtractEffectLighting(float3 inputColor, out float3 dirColor, out float3 am } # if defined(LIGHTING) -float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPosition, inout float shadowVariance) +float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPosition, uint eyeIndex, inout float shadowVariance) { float3 color = DLightColor.xyz * Color::EffectLightingMult(); bool suppressExternalEmittance = SharedData::InInterior && (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::SuppressExternalEmittance); @@ -518,7 +577,11 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo # if defined(SKYLIGHTING) float skylightingDiffuse = 1.0; if (!SharedData::InInterior) { +# if defined(VR) + float3 positionMSSkylight = worldPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMSSkylight = worldPosition; +# endif sh2 skylightingSH = Skylighting::SampleNoBias(positionMSSkylight); skylightingDiffuse = Skylighting::EvaluateDiffuse(skylightingSH, float3(0, 0, 1), Skylighting::GetFadeOutFactor(positionMSSkylight)); @@ -541,7 +604,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); if (inWorld && ShadowSampling::HasDirectionalShadows()) - dirShadow = ShadowSampling::Get3DFilteredShadow(worldPosition.xyz, viewDirection, screenPosition, unusedSurfaceShadow); + dirShadow = ShadowSampling::Get3DFilteredShadow(worldPosition.xyz, viewDirection, screenPosition, eyeIndex, unusedSurfaceShadow); shadowVariance = 1.0 - sqrt(saturate(fwidth(dirShadow))); @@ -549,7 +612,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif @@ -570,7 +633,7 @@ float3 GetLightingColor(float3 msPosition, float3 worldPosition, float2 screenPo if (!(Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld)) # endif { - float4 lightDistanceSquared = (PLightPositionX[0] - msPosition.xxxx) * (PLightPositionX[0] - msPosition.xxxx) + (PLightPositionY[0] - msPosition.yyyy) * (PLightPositionY[0] - msPosition.yyyy) + (PLightPositionZ[0] - msPosition.zzzz) * (PLightPositionZ[0] - msPosition.zzzz); + float4 lightDistanceSquared = (PLightPositionX[eyeIndex] - msPosition.xxxx) * (PLightPositionX[eyeIndex] - msPosition.xxxx) + (PLightPositionY[eyeIndex] - msPosition.yyyy) * (PLightPositionY[eyeIndex] - msPosition.yyyy) + (PLightPositionZ[eyeIndex] - msPosition.zzzz) * (PLightPositionZ[eyeIndex] - msPosition.zzzz); float4 lightFadeMul = 1.0.xxxx - saturate(PLightingRadiusInverseSquared * lightDistanceSquared); color.x += dot(Color::PointLight(PLightColorR.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); color.y += dot(Color::PointLight(PLightColorG.xxx).x * lightFadeMul * Color::EffectLightingMult(), 1.0.xxxx); @@ -610,7 +673,7 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi for (uint i = 0; i < sampleCount; i++) { float t = (float(i) + noise) * rcpSampleCount; float3 samplePositionWS = lerp(startPosition, endPosition, t); - shadow += ShadowSampling::GetWorldShadow(samplePositionWS, FrameBuffer::CameraPosAdjust.xyz); + shadow += ShadowSampling::GetWorldShadow(samplePositionWS, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); } shadow *= rcpSampleCount; } @@ -621,7 +684,7 @@ float3 GetLightingShadow(float3 color, float3 worldPosition, float2 screenPositi # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif @@ -633,6 +696,12 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout = (PS_OUTPUT)0; +# if !defined(VR) + uint eyeIndex = 0; +# else + uint eyeIndex = input.EyeIndex; +# endif // !VR + float4 fogMul = 1; # if !defined(MULTBLEND) fogMul.xyz = input.FogAlpha; @@ -688,7 +757,7 @@ PS_OUTPUT main(PS_INPUT input) float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x); # if defined(LIGHTING) - propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, shadowVariance); + propertyColor = GetLightingColor(input.MSPosition.xyz, input.WorldPosition.xyz, input.Position.xy, eyeIndex, shadowVariance); # if defined(LIGHT_LIMIT_FIX) float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; @@ -865,7 +934,7 @@ PS_OUTPUT main(PS_INPUT input) float3 vanillaFogColor = fogColor; float expFogFactor = 0; if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); expFogFactor = exponentialHeightFog.w; # if defined(ADDBLEND) || defined(MULTBLEND) || defined(MULTBLEND_DECAL) fogColor = exponentialHeightFog.xyz; @@ -936,7 +1005,7 @@ PS_OUTPUT main(PS_INPUT input) float3 screenSpaceNormal = normalize(input.ScreenSpaceNormal); # endif psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), 0.0, psout.Diffuse.w); - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); psout.MotionVectors = float4(screenMotionVector, 0.0, psout.Diffuse.w); # endif @@ -953,7 +1022,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # elif defined(MOTIONVECTORS_NORMALS) - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); psout.MotionVectors = screenMotionVector; # if (defined(MEMBRANE) && defined(SKINNED) && defined(NORMALS)) diff --git a/package/Shaders/ISApplyVolumetricLighting.hlsl b/package/Shaders/ISApplyVolumetricLighting.hlsl index aa5d9d0dd1..6d42ead969 100644 --- a/package/Shaders/ISApplyVolumetricLighting.hlsl +++ b/package/Shaders/ISApplyVolumetricLighting.hlsl @@ -1,5 +1,6 @@ #include "Common/DummyVSTexCoord.hlsl" #include "Common/FrameBuffer.hlsli" +#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -38,6 +39,12 @@ PS_OUTPUT main(PS_INPUT input) float2 screenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); float depth = DepthTex.Sample(DepthSampler, screenPosition).x; +# ifdef VR + if (depth < 0.0001) { // not a valid location + psout.VL = 0.0; + return psout; + } +# endif float repartition = clamp(RepartitionTex.SampleLevel(RepartitionSampler, depth, 0).x, 0, 0.9999); float vl = g_IntensityX_TemporalY.x * VLTex.SampleLevel(VLSampler, float3(input.TexCoord, repartition), 0).x; @@ -47,11 +54,33 @@ PS_OUTPUT main(PS_INPUT input) if (0.001 < g_IntensityX_TemporalY.y) { float2 motionVector = MotionVectorsTex.Sample(MotionVectorsSampler, screenPosition).xy; +# ifdef VR + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); + float2 previousTexCoord = Stereo::ConvertFromStereoUV(input.TexCoord, eyeIndex); + previousTexCoord += motionVector; + bool isValid = previousTexCoord.x >= 0 && previousTexCoord.x < 1 && previousTexCoord.y >= 0 && previousTexCoord.y < 1; + previousTexCoord = Stereo::ConvertToStereoUV(previousTexCoord, eyeIndex); +# else float2 previousTexCoord = input.TexCoord + motionVector; bool isValid = previousTexCoord.x >= 0 && previousTexCoord.x < 1 && previousTexCoord.y >= 0 && previousTexCoord.y < 1; +# endif float2 previousScreenPosition = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(previousTexCoord); float previousVl = PreviousFrameTex.Sample(PreviousFrameSampler, previousScreenPosition).x; - float previousDepth = PreviousDepthTex.Sample(PreviousDepthSampler, previousScreenPosition).x; + float previousDepth = PreviousDepthTex.Sample(PreviousDepthSampler, +# ifndef VR + previousScreenPosition +# else + // In VR with dynamic resolution enabled, there's a bug with the depth stencil. + // The depth stencil from ISDepthBufferCopy is actually full size and not scaled. + // Thus there's never a need to scale it down. + previousTexCoord +# endif + ) + .x; + +# ifdef VR + isValid = isValid && abs(previousDepth) > 0.0001; +# endif float temporalContribution = g_IntensityX_TemporalY.y * (1 - smoothstep(0, 1, min(1, 100 * abs(depth - previousDepth)))); psout.VL = lerp(adjustedVl, previousVl, temporalContribution * isValid); diff --git a/package/Shaders/ISFullScreenVR.hlsl b/package/Shaders/ISFullScreenVR.hlsl new file mode 100644 index 0000000000..455602e3c9 --- /dev/null +++ b/package/Shaders/ISFullScreenVR.hlsl @@ -0,0 +1,69 @@ +#include "Common/DummyVSTexCoord.hlsl" +#include "Common/VR.hlsli" + +typedef VS_OUTPUT PS_INPUT; + +struct PS_OUTPUT +{ + float4 Color: SV_Target0; // Final color output for the pixel shader. +}; + +#if defined(PSHADER) +SamplerState ImageSampler : register(s0); // Sampler state for texture sampling. +Texture2D ImageTex : register(t0); // Texture to sample colors from. + +cbuffer PerGeometry : register(b2) +{ + float4 FullScreenColor; // Color applied to the final output, used for tinting or blending effects. + float4 Params0; // General parameters; may include scaling or offset values. + float4 Params1; // Length parameters for scaling or thresholding; Params1.z represents `g_flTime`. + float4 UpsampleParams; // Dynamic resolution parameters: + // - UpsampleParams.x: fDynamicResolutionWidthRatio + // - UpsampleParams.y: fDynamicResolutionHeightRatio + // - UpsampleParams.z: fDynamicResolutionPreviousWidthRatio + // - UpsampleParams.w: fDynamicResolutionPreviousHeightRatio +}; + +// Function to generate noise using Valve's ScreenSpaceDither method. +// References: +// - https://blog.frost.kiwi/GLSL-noise-and-radial-gradient/ +// - https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf +float3 ScreenSpaceDither(float2 vScreenPos) +{ + // Iestyn's RGB dither (7 asm instructions) from Portal 2 X360, + // slightly modified for VR applications. + float3 vDither = dot(float2(171.0, 231.0), vScreenPos.xy + Params1.zz).xxx; + vDither.rgb = frac(vDither.rgb / float3(103.0, 71.0, 97.0)) - float3(0.5, 0.5, 0.5); + return (vDither.rgb / 255.0) * 0.375; // Normalize dither values to a suitable range. +} + +PS_OUTPUT main(PS_INPUT input) +{ + PS_OUTPUT psout; + + float2 uv = input.TexCoord; // Get the UV coordinates from input. + + // Convert UV to normalized screen space [-1, 1]. + float2 normalizedScreenCoord = Stereo::ConvertUVToNormalizedScreenSpace(uv); + + // Calculate the length of the normalized screen coordinates. + float normalizedLength = saturate(Params1.x * (length(normalizedScreenCoord) - Params1.y) * Params0.x); + + // Upsample and clamp texture coordinates based on dynamic resolution. + float2 uvScaled = min(UpsampleParams.zw, UpsampleParams.xy * uv.xy); // Clamp UVs to prevent overflow. + float3 sampledColor = ImageTex.Sample(ImageSampler, uvScaled).xyz; // Sample color from the texture. + + // Manipulate the sampled color based on the normalized length. + float3 finalColor = sampledColor * (1.0 + normalizedLength); // Scale sampled color. + + // Generate noise to apply to the final color. + float3 noise = ScreenSpaceDither(input.Position.xy); + finalColor += Params0.yyy * noise * Params1.www; // Adjust final color with noise. + + // Final color manipulation: blend final color with FullScreenColor. + psout.Color.xyz = lerp(finalColor, FullScreenColor.xyz, FullScreenColor.www); // Blend based on the alpha component. + psout.Color.w = 1.0; // Set alpha to full opacity. + + return psout; // Return the pixel shader output. +} +#endif diff --git a/package/Shaders/ISReflectionsRayTracing.hlsl b/package/Shaders/ISReflectionsRayTracing.hlsl index babe64b887..3d4986ca5f 100644 --- a/package/Shaders/ISReflectionsRayTracing.hlsl +++ b/package/Shaders/ISReflectionsRayTracing.hlsl @@ -2,6 +2,7 @@ #include "Common/FrameBuffer.hlsli" #include "Common/MotionBlur.hlsli" #include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" typedef VS_OUTPUT PS_INPUT; @@ -68,12 +69,12 @@ int GetSSRBinaryIterations(int raymarchIterations) float2 ConvertRaySample(float2 raySample, uint eyeIndex) { - return FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(raySample); + return FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(Stereo::ConvertToStereoUV(raySample, eyeIndex)); } -float2 ConvertRaySamplePrevious(float2 raySample) +float2 ConvertRaySamplePrevious(float2 raySample, uint eyeIndex) { - return FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(raySample); + return FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(Stereo::ConvertToStereoUV(raySample, eyeIndex)); } float4 GetReflectionColor( @@ -108,18 +109,21 @@ float4 GetReflectionColor( prevRaySample = raySample; raySample = projPosition + (float(i) / float(rayCount)) * projReflectionDirection; - float2 sampleUV = raySample.xy; + float2 sampleUV; + uint sampleEyeIndex; + Stereo::ResolveMonoUVForEye(raySample, eyeIndex, sampleUV, sampleEyeIndex); if (FrameBuffer::IsOutsideFrame(sampleUV)) return 0.0; - float iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV), 0).x; + float iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV, sampleEyeIndex), 0).x; if (saturate((raySample.z - iterationDepth) / SSRParams.y) > 0.0) { float3 binaryMinRaySample = prevRaySample; float3 binaryMaxRaySample = raySample; float3 binaryRaySample = raySample; float depthThicknessFactor; + uint hitEyeIndex = sampleEyeIndex; # if defined(VR) [loop] for (int k = 0; k < binCount; k++) @@ -129,8 +133,8 @@ float4 GetReflectionColor( # endif binaryRaySample = lerp(binaryMinRaySample, binaryMaxRaySample, 0.5); - sampleUV = binaryRaySample.xy; - iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV), 0).x; + Stereo::ResolveMonoUVForEye(binaryRaySample, eyeIndex, sampleUV, hitEyeIndex); + iterationDepth = DepthTex.SampleLevel(DepthSampler, ConvertRaySample(sampleUV, hitEyeIndex), 0).x; // Compute expected depth vs actual depth depthThicknessFactor = 1.0 - saturate(abs(binaryRaySample.z - iterationDepth) / SSRParams.y); @@ -146,8 +150,17 @@ float4 GetReflectionColor( float2 uvResultScreenCenterOffset = binaryRaySample.xy - 0.5; +# ifdef VR float2 centerDistance = abs(uvResultScreenCenterOffset.xy * 2.0); + // Make VR fades consistent by taking the closer of the two eyes + // Based on concepts from https://cuteloong.github.io/publications/scssr24/ + float2 otherEyeUvResultScreenCenterOffset = Stereo::ConvertMonoUVToOtherEye(float3(binaryRaySample.xy, iterationDepth), eyeIndex).xy - 0.5; + centerDistance = min(centerDistance, abs(otherEyeUvResultScreenCenterOffset * 2.0)); +# else + float2 centerDistance = abs(uvResultScreenCenterOffset.xy * 2.0); +# endif + // Fade out around screen edges float centerDistanceFadeFactorX = smoothstep(0.0, 0.1, saturate(1.0 - centerDistance.x)); float centerDistanceFadeFactorY = smoothstep(0.0, 0.5, saturate(1.0 - centerDistance.y)); @@ -155,18 +168,21 @@ float4 GetReflectionColor( float fadeFactor = depthThicknessFactor * ssrMarchingRadiusFadeFactor * centerDistanceFadeFactorX * centerDistanceFadeFactorY; if (fadeFactor > 0.0) { - float2 finalSampleUV = binaryRaySample.xy; + // Resolve final UV in the eye that owns the hit + float2 finalSampleUV; + uint finalEyeIndex; + Stereo::ResolveMonoUVForEye(float3(binaryRaySample.xy, iterationDepth), eyeIndex, finalSampleUV, finalEyeIndex); - float3 color = ColorTex.SampleLevel(ColorSampler, ConvertRaySample(finalSampleUV), 0).xyz; + float3 color = ColorTex.SampleLevel(ColorSampler, ConvertRaySample(finalSampleUV, finalEyeIndex), 0).xyz; // Final sample to world-space float4 positionWS = float4(float2(finalSampleUV.x, 1.0 - finalSampleUV.y) * 2.0 - 1.0, iterationDepth, 1.0); - positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse[finalEyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; positionWS.w = 1.0; // Compute camera motion vector - float2 cameraMotionVector = MotionBlur::GetSSMotionVector(positionWS, positionWS); + float2 cameraMotionVector = MotionBlur::GetSSMotionVector(positionWS, positionWS, finalEyeIndex); // Reproject alpha from previous frame float2 reprojectedRaySample = finalSampleUV + cameraMotionVector; @@ -174,7 +190,7 @@ float4 GetReflectionColor( // Check that the reprojected data is within the frame if (!FrameBuffer::IsOutsideFrame(reprojectedRaySample.xy)) - alpha = float4(AlphaTex.SampleLevel(AlphaSampler, ConvertRaySamplePrevious(reprojectedRaySample.xy), 0).xyz, 1.0); + alpha = float4(AlphaTex.SampleLevel(AlphaSampler, ConvertRaySamplePrevious(reprojectedRaySample.xy, finalEyeIndex), 0).xyz, 1.0); float3 reflectionColor = color + SSRParams.z * alpha.xyz * alpha.w; return float4(reflectionColor, fadeFactor * fovWeight); @@ -197,6 +213,7 @@ PS_OUTPUT main(PS_INPUT input) return psout; # endif + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); float2 uv = input.TexCoord; float2 screenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(uv); @@ -224,7 +241,7 @@ PS_OUTPUT main(PS_INPUT input) float depth = DepthTex.SampleLevel(DepthSampler, screenPosition, 0).x; float4 positionVS = float4(float2(uv.x, 1.0 - uv.y) * 2.0 - 1.0, depth, 1.0); - positionVS = mul(FrameBuffer::CameraProjInverse, positionVS); + positionVS = mul(FrameBuffer::CameraProjInverse[eyeIndex], positionVS); positionVS.xyz = positionVS.xyz / positionVS.w; float3 viewPosition = positionVS.xyz; @@ -238,7 +255,7 @@ PS_OUTPUT main(PS_INPUT input) } float4 reflectionPosition = float4(viewPosition + reflectionDirection, 1.0); - float4 projReflectionPosition = mul(FrameBuffer::CameraProj, reflectionPosition); + float4 projReflectionPosition = mul(FrameBuffer::CameraProj[eyeIndex], reflectionPosition); projReflectionPosition /= projReflectionPosition.w; projReflectionPosition.xy = projReflectionPosition.xy * float2(0.5, -0.5) + float2(0.5, 0.5); diff --git a/package/Shaders/ISSAOComposite.hlsl b/package/Shaders/ISSAOComposite.hlsl index 90e9edf6a8..ecaf54ce1e 100644 --- a/package/Shaders/ISSAOComposite.hlsl +++ b/package/Shaders/ISSAOComposite.hlsl @@ -157,11 +157,13 @@ PS_OUTPUT main(PS_INPUT input) } float snowMask = 0; +# if !defined(VR) if (EyePosition.w != 0) { float2 specSnow = snowSpecAlphaTex.Sample(snowSpecAlphaSampler, screenPosition).xy; composedColor.xyz += specSnow.x * specSnow.y; snowMask = specSnow.y; } +# endif # if defined(APPLY_SAO) if (EyePosition.w != 0 && 1e-5 < snowMask) { @@ -187,14 +189,15 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) bool exponentialHeightFogEnabled = SharedData::exponentialHeightFogSettings.enabled; - float2 monoUV = input.TexCoord.xy; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord.xy); + float2 monoUV = Stereo::ConvertFromStereoUV(input.TexCoord.xy, eyeIndex); float4 positionWS = float4(2 * float2(monoUV.x, -monoUV.y + 1) - 1, depth, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; float4 exponentialHeightFog = (float4)0; if (exponentialHeightFogEnabled) { - float4 fogScreenPosition = float4(monoUV * SharedData::BufferDim.xy, depth, 1.0f); - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, fogScreenPosition); + float4 fogScreenPosition = float4(Stereo::ConvertToStereoUV(monoUV, eyeIndex) * SharedData::BufferDim.xy, depth, 1.0f); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(positionWS.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, fogScreenPosition); } if (isGeometryDepth || exponentialHeightFogEnabled) { float fogFade = exponentialHeightFogEnabled ? ExponentialHeightFog::GetVanillaFogFade(FogNearColor.w) : FogNearColor.w; @@ -217,15 +220,16 @@ PS_OUTPUT main(PS_INPUT input) # endif # endif +# if !defined(VR) float sparklesInput = 0; if (EyePosition.w != 0 && snowMask != 0 && 1e-5 < SparklesParameters2.z) { float shadowMask = shadowMaskTex.SampleLevel(shadowMaskSampler, screenPosition, 0).x; float4 vsPosition = float4(2 * input.TexCoord.x - 1, 1 - 2 * input.TexCoord.y, depth, 1); - float4 csPosition = mul(FrameBuffer::CameraViewProjInverse, vsPosition); + float4 csPosition = mul(FrameBuffer::CameraViewProjInverse[0], vsPosition); csPosition.xyz /= csPosition.w; - csPosition.xyz += FrameBuffer::CameraPosAdjust.xyz; + csPosition.xyz += FrameBuffer::CameraPosAdjust[0].xyz; float3 noiseSeed = 0.07 * (SparklesParameters2.x * csPosition.xyz); float noiseValue = 0.5 * (SimplexNoise(noiseSeed) + 1); @@ -244,6 +248,7 @@ PS_OUTPUT main(PS_INPUT input) composedColor *= 1 - SparklesParameters2.w; composedColor += sparklesColor; +# endif psout.Color = composedColor; diff --git a/package/Shaders/ISSAOMinify.hlsl b/package/Shaders/ISSAOMinify.hlsl index 3e8a31211c..e82377f61c 100644 --- a/package/Shaders/ISSAOMinify.hlsl +++ b/package/Shaders/ISSAOMinify.hlsl @@ -44,7 +44,7 @@ PS_OUTPUT main(PS_INPUT input) float2 drAdjustedTexCoord = FrameBuffer::DynamicResolutionParams1.xy * input.TexCoord; float2 minifiedTexCoord = GetMinifiedTexCoord(drAdjustedTexCoord); finalTexCoord = clamp(minifiedTexCoord, 0, - FrameBuffer::DynamicResolutionParams1.xy - float2(FrameBuffer::CameraPreviousPosAdjust.w, 0)); + FrameBuffer::DynamicResolutionParams1.xy - float2(FrameBuffer::CameraPreviousPosAdjust[0].w, 0)); } else { finalTexCoord = GetMinifiedTexCoord(input.TexCoord); } diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index 4b71469bf0..cf90a30f03 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -537,6 +537,7 @@ PS_OUTPUT main(PS_INPUT input) feedbackOut.x = feedbackLumaOut; // Vanilla writes opaque alpha unconditionally on both SE and VR (decompile o0.w = 1). colorOut.w = 1; +# endif feedbackOut.w = 1; # ifdef HDR_OUTPUT diff --git a/package/Shaders/ISVolumetricLightingGenerateCS.hlsl b/package/Shaders/ISVolumetricLightingGenerateCS.hlsl index 5cab1319fb..7053aedfee 100644 --- a/package/Shaders/ISVolumetricLightingGenerateCS.hlsl +++ b/package/Shaders/ISVolumetricLightingGenerateCS.hlsl @@ -1,5 +1,6 @@ #include "Common/Math.hlsli" #include "Common/Random.hlsli" +#include "Common/VR.hlsli" #if defined(CSHADER) SamplerState ShadowmapSampler : register(s0); @@ -23,21 +24,39 @@ RWTexture3D DensityRW : register(u0); cbuffer PerTechnique : register(b0) { - row_major float4x4 CameraViewProj : packoffset(c0); - row_major float4x4 CameraViewProjInverse : packoffset(c4); - float4x3 ShadowMapProj[3] : packoffset(c8); +# ifndef VR + row_major float4x4 CameraViewProj[1] : packoffset(c0); + row_major float4x4 CameraViewProjInverse[1] : packoffset(c4); + float4x3 ShadowMapProj[1][3] : packoffset(c8); float3 EndSplitDistances : packoffset(c17.x); float ShadowMapCount : packoffset(c17.w); float EnableShadowCasting : packoffset(c18); float3 DirLightDirection : packoffset(c19); float3 TextureDimensions : packoffset(c20); - float3 WindInput : packoffset(c21); + float3 WindInput[1] : packoffset(c21); float InverseDensityScale : packoffset(c21.w); - float3 PosAdjust : packoffset(c22); + float3 PosAdjust[1] : packoffset(c22); float IterationIndex : packoffset(c22.w); float PhaseContribution : packoffset(c23.x); float PhaseScattering : packoffset(c23.y); float DensityContribution : packoffset(c23.z); +# else + row_major float4x4 CameraViewProj[2] : packoffset(c0); + row_major float4x4 CameraViewProjInverse[2] : packoffset(c8); + float4x3 ShadowMapProj[2][3] : packoffset(c16); + float3 EndSplitDistances : packoffset(c34.x); + float ShadowMapCount : packoffset(c34.w); + float EnableShadowCasting : packoffset(c35.x); + float3 DirLightDirection : packoffset(c36); + float3 TextureDimensions : packoffset(c37); + float3 WindInput[2] : packoffset(c38); + float InverseDensityScale : packoffset(c39.w); + float3 PosAdjust[2] : packoffset(c40); + float IterationIndex : packoffset(c41.w); + float PhaseContribution : packoffset(c42.x); + float PhaseScattering : packoffset(c42.y); + float DensityContribution : packoffset(c42.z); +# endif } [numthreads(32, 32, 1)] void main(uint3 dispatchID : SV_DispatchThreadID) { @@ -53,14 +72,16 @@ cbuffer PerTechnique : register(b0) }; float3 normalizedCoordinates = float3(dispatchID.xy + 0.5, dispatchID.z - 1.0) * rcp(TextureDimensions.xyz); - float3 depthUv = normalizedCoordinates + StepCoefficients[IterationIndex]; + float2 uv = normalizedCoordinates.xy; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + float3 depthUv = Stereo::ConvertFromStereoUV(normalizedCoordinates, eyeIndex) + StepCoefficients[IterationIndex]; float depth = InverseRepartitionTex.SampleLevel(InverseRepartitionSampler, depthUv.z, 0); float4 positionCS = float4(2 * depthUv.x - 1, 1 - 2 * depthUv.y, depth, 1); - float4 positionWS = mul(CameraViewProjInverse, positionCS); + float4 positionWS = mul(CameraViewProjInverse[eyeIndex], positionCS); positionWS *= rcp(positionWS.w); - float4 positionCSShifted = mul(CameraViewProj, positionWS); + float4 positionCSShifted = mul(CameraViewProj[eyeIndex], positionWS); positionCSShifted *= rcp(positionCSShifted.w); float shadowMapDepth = positionCSShifted.z; @@ -70,7 +91,7 @@ cbuffer PerTechnique : register(b0) uint cascadeIndex = ShadowMapCount >= 3.0f && shadowMapDepth > EndSplitDistances.y ? 2 : shadowMapDepth > EndSplitDistances.x ? 1 : 0; float shadowMapThreshold = cascadeIndex == 0 ? 0.01f : 0.0f; - float4x3 lightProjectionMatrix = ShadowMapProj[cascadeIndex]; + float4x3 lightProjectionMatrix = ShadowMapProj[eyeIndex][cascadeIndex]; float3 positionLS = mul(transpose(lightProjectionMatrix), float4(positionWS.xyz, 1)).xyz; float shadowMapValue = ShadowmapTex.SampleLevel(ShadowmapSampler, float3(positionLS.xy, cascadeIndex), 0); @@ -82,7 +103,7 @@ cbuffer PerTechnique : register(b0) } } - float3 noiseUv = 0.0125 * (InverseDensityScale * (positionWS.xyz + WindInput)); + float3 noiseUv = 0.0125 * (InverseDensityScale * (positionWS.xyz + WindInput[eyeIndex])); float noise = NoiseTex.SampleLevel(NoiseSampler, noiseUv, 0); float densityFactor = noise * (1 - 0.75 * smoothstep(0, 1, saturate(2 * positionWS.z / 300))); float densityContribution = lerp(1, densityFactor, DensityContribution); @@ -94,7 +115,7 @@ cbuffer PerTechnique : register(b0) float shadowContribution = noShadow; # if defined(TERRAIN_SHADOWS) || defined(CLOUD_SHADOWS) - shadowContribution *= sqrt(ShadowSampling::GetWorldShadow(positionWS.xyz, PosAdjust)); + shadowContribution *= sqrt(ShadowSampling::GetWorldShadow(positionWS.xyz, PosAdjust[eyeIndex], eyeIndex)); # endif float vl = shadowContribution * densityContribution * phaseContribution; diff --git a/package/Shaders/ISWaterBlend.hlsl b/package/Shaders/ISWaterBlend.hlsl index c91411ad20..f3d9018d4a 100644 --- a/package/Shaders/ISWaterBlend.hlsl +++ b/package/Shaders/ISWaterBlend.hlsl @@ -43,6 +43,7 @@ namespace WaterBlend PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(input.TexCoord); float2 adjustedScreenPosition = FrameBuffer::GetDynamicResolutionAdjustedScreenPosition(input.TexCoord); float waterMask = waterMaskTex.Sample(waterMaskSampler, adjustedScreenPosition).z; if (waterMask < WaterBlend::WaterMaskThreshold) { @@ -51,14 +52,17 @@ PS_OUTPUT main(PS_INPUT input) float3 sourceColor = sourceTex.Sample(sourceSampler, adjustedScreenPosition).xyz; float2 motion = motionBufferTex.Sample(motionBufferSampler, adjustedScreenPosition).xy; - float2 motionScreenPosition = input.TexCoord + motion; + float2 motionScreenPosition = Stereo::ConvertToStereoUV(Stereo::ConvertFromStereoUV(input.TexCoord, eyeIndex) + motion, eyeIndex); float2 motionAdjustedScreenPosition = FrameBuffer::GetPreviousDynamicResolutionAdjustedScreenPosition(motionScreenPosition); float4 waterHistory = waterHistoryTex.Sample(waterHistorySampler, motionAdjustedScreenPosition).xyzw; float3 finalColor = sourceColor; - if (motionScreenPosition.x >= 0 && motionScreenPosition.y >= 0 && motionScreenPosition.x <= 1 && + if ( +# ifndef VR + motionScreenPosition.x >= 0 && motionScreenPosition.y >= 0 && motionScreenPosition.x <= 1 && +# endif motionScreenPosition.y <= 1 && waterHistory.w > 0.0) { float historyFactor = 0.95; if (NearFar_Menu_DistanceFactor.z == 0) { diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index f076fc0e05..c725747226 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -11,6 +11,7 @@ #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" #include "Common/Triplanar.hlsli" +#include "Common/VR.hlsli" #if defined(FACEGEN) || defined(FACEGEN_RGB_TINT) # define SKIN @@ -51,6 +52,9 @@ struct VS_INPUT #if defined(EYE) float EyeParameter: TEXCOORD2; #endif // EYE +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -86,16 +90,28 @@ struct VS_OUTPUT float4 Color: COLOR0; float4 FogParam: COLOR1; +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 +#endif + float3 ModelPosition: TEXCOORD12; }; #ifdef VSHADER cbuffer PerTechnique : register(b0) { - float4 HighDetailRange : packoffset(c0); // loaded cells center in xy, size in zw +# if !defined(VR) + float4 HighDetailRange[1] : packoffset(c0); // loaded cells center in xy, size in zw float4 FogParam : packoffset(c1); float4 FogNearColor : packoffset(c2); float4 FogFarColor : packoffset(c3); +# else + float4 HighDetailRange[2] : packoffset(c0); // loaded cells center in xy, size in zw + float4 FogParam : packoffset(c2); + float4 FogNearColor : packoffset(c3); + float4 FogFarColor : packoffset(c4); +# endif // VR }; cbuffer PerMaterial : register(b1) @@ -107,25 +123,46 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { - row_major float3x4 World : packoffset(c0); - row_major float3x4 PreviousWorld : packoffset(c3); - float4 EyePosition : packoffset(c6); +# if !defined(VR) + row_major float3x4 World[1] : packoffset(c0); + row_major float3x4 PreviousWorld[1] : packoffset(c3); + float4 EyePosition[1] : packoffset(c6); float4 LandBlendParams : packoffset(c7); // offset in xy, gridPosition in yw float4 TreeParams : packoffset(c8); // wind magnitude in y, amplitude in z, leaf frequency in w float2 WindTimers : packoffset(c9); - row_major float3x4 TextureProj : packoffset(c10); + row_major float3x4 TextureProj[1] : packoffset(c10); float IndexScale : packoffset(c13); float4 WorldMapOverlayParameters : packoffset(c14); +# else // VR has 49 vs 30 entries + row_major float3x4 World[2] : packoffset(c0); + row_major float3x4 PreviousWorld[2] : packoffset(c6); + float4 EyePosition[2] : packoffset(c12); + float4 LandBlendParams : packoffset(c14); // offset in xy, gridPosition in yw + float4 TreeParams : packoffset(c15); // wind magnitude in y, amplitude in z, leaf frequency in w + float2 WindTimers : packoffset(c16); + row_major float3x4 TextureProj[2] : packoffset(c17); + float IndexScale : packoffset(c23); + float4 WorldMapOverlayParameters : packoffset(c24); +# endif // VR }; cbuffer VS_PerFrame : register(b12) { - row_major float3x3 ScreenProj : packoffset(c0); - row_major float4x4 ViewProj : packoffset(c8); -# if defined(SKINNED) - float3 BonesPivot : packoffset(c40); - float3 PreviousBonesPivot : packoffset(c41); -# endif // SKINNED +# if !defined(VR) + row_major float3x3 ScreenProj[1] : packoffset(c0); + row_major float4x4 ViewProj[1] : packoffset(c8); +# if defined(SKINNED) + float3 BonesPivot[1] : packoffset(c40); + float3 PreviousBonesPivot[1] : packoffset(c41); +# endif // SKINNED +# else + row_major float3x3 ScreenProj[2] : packoffset(c0); + row_major float4x4 ViewProj[2] : packoffset(c16); +# if defined(SKINNED) + float3 BonesPivot[2] : packoffset(c80); + float3 PreviousBonesPivot[2] : packoffset(c82); +# endif // SKINNED +# endif // VR }; # if defined(TREE_ANIM) @@ -145,8 +182,13 @@ VS_OUTPUT main(VS_INPUT input) precise float4 inputPosition = float4(input.Position.xyz, 1.0); + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif + ); # if defined(LODLANDNOISE) || defined(LODLANDSCAPE) - inputPosition = LodLandscape::AdjustLodLandscapeVertexPositionMS(inputPosition, float4x4(World, float4(0, 0, 0, 1)), HighDetailRange); + inputPosition = LodLandscape::AdjustLodLandscapeVertexPositionMS(inputPosition, float4x4(World[eyeIndex], float4(0, 0, 0, 1)), HighDetailRange[eyeIndex]); # endif // defined(LODLANDNOISE) || defined(LODLANDSCAPE) \ precise float4 previousInputPosition = inputPosition; @@ -163,19 +205,19 @@ VS_OUTPUT main(VS_INPUT input) precise int4 actualIndices = 765.01.xxxx * input.BoneIndices.xyzw; float3x4 previousWorldMatrix = - Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot, input.BoneWeights); + Skinned::GetBoneTransformMatrix(PreviousBones, actualIndices, PreviousBonesPivot[eyeIndex], input.BoneWeights); precise float4 previousWorldPosition = float4(mul(inputPosition, transpose(previousWorldMatrix)), 1); - float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot, input.BoneWeights); + float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, actualIndices, BonesPivot[eyeIndex], input.BoneWeights); precise float4 worldPosition = float4(mul(inputPosition, transpose(worldMatrix)), 1); - float4 viewPos = mul(ViewProj, worldPosition); + float4 viewPos = mul(ViewProj[eyeIndex], worldPosition); # else // !SKINNED - precise float4 previousWorldPosition = float4(mul(PreviousWorld, inputPosition), 1); - precise float4 worldPosition = float4(mul(World, inputPosition), 1); - precise float4x4 world4x4 = float4x4(World[0], World[1], World[2], float4(0, 0, 0, 1)); - precise float4x4 modelView = mul(ViewProj, world4x4); + precise float4 previousWorldPosition = float4(mul(PreviousWorld[eyeIndex], inputPosition), 1); + precise float4 worldPosition = float4(mul(World[eyeIndex], inputPosition), 1); + precise float4x4 world4x4 = float4x4(World[eyeIndex][0], World[eyeIndex][1], World[eyeIndex][2], float4(0, 0, 0, 1)); + precise float4x4 modelView = mul(ViewProj[eyeIndex], world4x4); float4 viewPos = mul(modelView, inputPosition); # endif // SKINNED @@ -189,8 +231,8 @@ VS_OUTPUT main(VS_INPUT input) # if defined(LANDSCAPE) vsout.TexCoord0.zw = (uv * 0.010416667.xx + LandBlendParams.xy) * float2(1, -1) + float2(0, 1); # elif defined(PROJECTED_UV) && !defined(SKINNED) - vsout.TexCoord0.z = mul(TextureProj[0], inputPosition); - vsout.TexCoord0.w = mul(TextureProj[1], inputPosition); + vsout.TexCoord0.z = mul(TextureProj[eyeIndex][0], inputPosition); + vsout.TexCoord0.w = mul(TextureProj[eyeIndex][1], inputPosition); # endif vsout.TexCoord0.xy = uv; @@ -220,9 +262,9 @@ VS_OUTPUT main(VS_INPUT input) vsout.TBN1.xyz = worldTbnTr[1]; vsout.TBN2.xyz = worldTbnTr[2]; # else - vsout.TBN0.xyz = mul(tbn, World[0].xyz); - vsout.TBN1.xyz = mul(tbn, World[1].xyz); - vsout.TBN2.xyz = mul(tbn, World[2].xyz); + vsout.TBN0.xyz = mul(tbn, World[eyeIndex][0].xyz); + vsout.TBN1.xyz = mul(tbn, World[eyeIndex][1].xyz); + vsout.TBN2.xyz = mul(tbn, World[eyeIndex][2].xyz); float3x3 tempTbnTr = transpose(float3x3(vsout.TBN0.xyz, vsout.TBN1.xyz, vsout.TBN2.xyz)); tempTbnTr[0] = normalize(tempTbnTr[0]); tempTbnTr[1] = normalize(tempTbnTr[1]); @@ -249,8 +291,8 @@ VS_OUTPUT main(VS_INPUT input) vsout.LandBlendWeights2.w = 1 - saturate(0.000375600968 * (9625.59961 - length(gridOffset))); vsout.LandBlendWeights2.xyz = input.LandBlendWeights2.xyz; # elif defined(PROJECTED_UV) && !defined(SKINNED) - float3x3 texProjWorld3x3 = float3x3(World[0].xyz, World[1].xyz, World[2].xyz); - vsout.TexProj = mul(texProjWorld3x3, TextureProj[2].xyz); + float3x3 texProjWorld3x3 = float3x3(World[eyeIndex][0].xyz, World[eyeIndex][1].xyz, World[eyeIndex][2].xyz); + vsout.TexProj = mul(texProjWorld3x3, TextureProj[eyeIndex][2].xyz); # endif # if defined(EYE) @@ -273,6 +315,13 @@ VS_OUTPUT main(VS_INPUT input) vsout.FogParam.xyz = lerp(FogNearColor.xyz, FogFarColor.xyz, fogColorParam); vsout.FogParam.w = fogColorParam; +# if defined(VR) + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); + vsout.Position = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR + vsout.ModelPosition = input.Position.xyz; return vsout; @@ -307,6 +356,13 @@ struct PS_OUTPUT #ifdef PSHADER +# if defined(VR_STEREO_OPT) && !defined(SNOW) +// POM depth offset UAV — written per-pixel for StereoBlendCS depth-aware reprojection. +// Bound at u7 (after the 7 deferred MRT slots 0-6) via OMSetRenderTargetsAndUnorderedAccessViews. +// -1.0 = no POM (sentinel, matches ClearPomOffsetTexture); >= 0 = POM ran (StereoBlendCS checks >= 0). +RWTexture2D PomOffsetTex : register(u7); +# endif + SamplerState SampTerrainParallaxSampler : register(s1); # if defined(LANDSCAPE) @@ -543,6 +599,7 @@ cbuffer PerMaterial : register(b1) # endif float4 CharacterLightParams : packoffset(c14); + // VR is [9] instead of [15] uint PBRFlags : packoffset(c15.x); float3 PBRParams1 : packoffset(c15.y); // roughness scale, displacement scale, specular level @@ -553,6 +610,7 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { +# if !defined(VR) float3 DirLightDirection : packoffset(c0); float3 DirLightColor : packoffset(c1); float4 ShadowLightMaskSelect : packoffset(c2); @@ -569,12 +627,34 @@ cbuffer PerGeometry : register(b2) float4 PointLightPosition[7] : packoffset(c15); // point light radius in w float4 PointLightColor[7] : packoffset(c22); float2 NumLightNumShadowLight : packoffset(c29); +# else + // VR is [49] instead of [30] + float3 DirLightDirection : packoffset(c0); + float4 UnknownPerGeometry[12] : packoffset(c1); + float3 DirLightColor : packoffset(c13); + float4 ShadowLightMaskSelect : packoffset(c14); + float4 MaterialData : packoffset(c15); // envmapLODFade in x, specularLODFade in y, alpha in z + float AlphaTestRef : packoffset(c16); + float3 EmitColor : packoffset(c16.y); + float4 ProjectedUVParams : packoffset(c18); + float4 SSRParams : packoffset(c19); + float4 WorldMapOverlayParametersPS : packoffset(c20); + float4 ProjectedUVParams2 : packoffset(c21); + float4 ProjectedUVParams3 : packoffset(c22); // fProjectedUVDiffuseNormalTilingScale in x, fProjectedUVNormalDetailTilingScale in y, EnableProjectedNormals in w + row_major float3x4 DirectionalAmbient : packoffset(c23); + float4 AmbientSpecularTintAndFresnelPower : packoffset(c26); // Fresnel power in z, color in xyz + float4 PointLightPosition[14] : packoffset(c27); // point light radius in w + float4 PointLightColor[7] : packoffset(c41); + float2 NumLightNumShadowLight : packoffset(c48); +# endif // VR }; +# if !defined(VR) cbuffer AlphaTestRefBuffer : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif float GetSoftLightMultiplier(float angle) { @@ -880,11 +960,12 @@ float GetSnowParameterY(float texProjTmp, float alpha) PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) { PS_OUTPUT psout; + uint eyeIndex = Stereo::GetEyeIndexPS(input.Position, VPOSOffset); - float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; + float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; float3 viewDirection = -normalize(input.WorldPosition.xyz); - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); # if defined(DEFERRED) @@ -955,6 +1036,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // LANDSCAPE float sh0 = 0; float pixelOffset = 0; +# if defined(VR_STEREO_OPT) && !defined(SNOW) + bool hasPOM = false; +# endif # if defined(EMAT) # if defined(LANDSCAPE) @@ -1011,7 +1095,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(PARALLAX) && (defined(SKINNED) || !defined(MODELSPACENORMALS)) if (SharedData::extendedMaterialSettings.EnableParallax) { mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1044,7 +1133,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (envMaskSample.w > kMaskEpsilon && envMaskSample.w < (1.0 - kMaskEpsilon)) { complexMaterialParallax = true; mipLevel = ExtendedMaterials::GetMipLevel(uv, TexEnvMaskSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, viewDirection, tbnTr, screenNoise, TexEnvMaskSampler, SampTerrainParallaxSampler, 3, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexEnvMaskSampler.SampleLevel(SampEnvMaskSampler, uv, mipLevel).w; complexMaterialColor = TexEnvMaskSampler.Sample(SampEnvMaskSampler, uv); @@ -1091,7 +1185,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) displacementParams.HeightScale *= PBRParams1.y; } mipLevel = ExtendedMaterials::GetMipLevel(uv, TexParallaxSampler, screenNoise); - uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset); + uv = ExtendedMaterials::GetParallaxCoords(viewPosition.z, uv, mipLevel, refractedViewDirection, tbnTr, screenNoise, TexParallaxSampler, SampParallaxSampler, 0, displacementParams, pixelOffset +# if defined(VR_STEREO_OPT) && !defined(SNOW) + , + hasPOM +# endif + ); if (SharedData::extendedMaterialSettings.EnableShadows && (parallaxShadowQuality > 0.0f || SharedData::extendedMaterialSettings.ExtendShadows)) sh0 = TexParallaxSampler.SampleLevel(SampParallaxSampler, uv, mipLevel).x; } @@ -1187,9 +1286,15 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) weights[0] = weights[1] = weights[2] = weights[3] = weights[4] = weights[5] = 0.0; # if defined(TERRAIN_VARIATION) uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, sharedOffset, dx, dy, pixelOffset, +# if defined(VR_STEREO_OPT) && !defined(SNOW) + hasPOM, +# endif weights); # else uv = ExtendedMaterials::GetParallaxCoords(input, viewPosition.z, uv, mipLevels, viewDirection, tbnTr, screenNoise, displacementParams, pixelOffset, +# if defined(VR_STEREO_OPT) && !defined(SNOW) + hasPOM, +# endif weights); # endif if (SharedData::extendedMaterialSettings.EnableHeightBlending) { @@ -1977,9 +2082,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # else float3 worldNormal = normalize(mul(tbn, normal.xyz)); # if defined(TREE_ANIM) - float3 viewNormal = normalize(FrameBuffer::WorldToView(worldNormal, false)); + float3 viewNormal = normalize(FrameBuffer::WorldToView(worldNormal, false, eyeIndex)); viewNormal = float3(viewNormal.xy, -abs(viewNormal.z)); - worldNormal = normalize(FrameBuffer::ViewToWorld(viewNormal, false)); + worldNormal = normalize(FrameBuffer::ViewToWorld(viewNormal, false, eyeIndex)); # endif # if defined(SPARKLE) @@ -2045,7 +2150,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float projWeight = 0; # if defined(PROJECTED_UV) - float3 projWorldPos = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; + float3 projWorldPos = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; float3 triFaceNormal = normalize(-cross(ddx(input.WorldPosition.xyz), ddy(input.WorldPosition.xyz))); float3 triWeights = Triplanar::GetWeights(tbnTr[2], triFaceNormal); float projNoise = Triplanar::Sample(TexCharacterLightProjNoiseSampler, SampCharacterLightProjNoiseSampler, projWorldPos, triWeights, ProjectedUVParams.z).x; @@ -2119,7 +2224,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 vertexNormal = worldNormal; # endif - float3 screenSpaceNormal = normalize(FrameBuffer::WorldToView(worldNormal, false)); + float3 screenSpaceNormal = normalize(FrameBuffer::WorldToView(worldNormal, false, eyeIndex)); # if defined(HAIR) && defined(CS_HAIR) float3 Bitangent = normalize(float3(input.TBN0.y, input.TBN1.y, input.TBN2.y)); @@ -2134,7 +2239,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::hairSpecularSettings.Enabled) { if (SharedData::hairSpecularSettings.EnableTangentShift && SharedData::hairSpecularSettings.HairMode != 1) { float3 shiftedNormal = Hair::ShiftWorldNormal(hairT, worldNormal, 0, uv); - screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(shiftedNormal, false, eyeIndex)); } } # endif @@ -2396,7 +2501,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float porosity = 1.0; # if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMSSkylight = input.WorldPosition.xyz; +# endif # if defined(DEFERRED) sh2 skylightingSH = Skylighting::Sample(positionMSSkylight, worldNormal); # else @@ -2405,7 +2514,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif - float4 waterData = SharedData::GetWaterData(input.WorldPosition.xyz); + float4 waterData = SharedData::GetWaterData(input.WorldPosition.xyz, eyeIndex); float waterHeight = waterData.w; float waterRoughnessSpecular = 1; @@ -2440,9 +2549,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(SKINNED) float3 ripplePosition = input.ModelPosition.xyz; # elif defined(DEFERRED) - float3 ripplePosition = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; + float3 ripplePosition = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; # else - float3 ripplePosition = !FrameBuffer::FrameParams.y ? input.ModelPosition.xyz : input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz; + float3 ripplePosition = !FrameBuffer::FrameParams.y ? input.ModelPosition.xyz : input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz; # endif raindropInfo = WetnessEffects::GetRainDrops(ripplePosition, SharedData::wetnessEffectsSettings.Time, wetnessNormal, flatnessAmount); } @@ -2457,7 +2566,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(CS_SKIN) && !defined(SKIN) if (skinEnabled) { - float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust.z, worldNormal.xyz); + float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); # if defined(TRUE_PBR) dynamicWetness.x = lerp(dynamicWetness.x, 0.0f, material.Metallic); # endif @@ -2477,7 +2586,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(SKINNED) && !(defined(SKIN) && defined(CS_SKIN)) if (wetness > 0.0 || puddleWetness > 0.0) { - float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust.xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; + float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; puddle = Random::perlinNoise(puddleCoords) * 0.5 + 0.5; puddle = puddle * ((minWetnessAngle / SharedData::wetnessEffectsSettings.PuddleMaxAngle) * SharedData::wetnessEffectsSettings.MaxPuddleWetness * 0.25) + 0.5; puddle *= lerp(wetness, puddleWetness, saturate(puddle - 0.25)); @@ -2524,17 +2633,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif # if defined(WATER_EFFECTS) - dirLightColor *= WaterEffects::ComputeCaustics(waterData, input.WorldPosition.xyz); + dirLightColor *= WaterEffects::ComputeCaustics(waterData, input.WorldPosition.xyz, eyeIndex); # endif // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (inWorld || inReflection) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); float dirLightAngle = dot(worldNormal.xyz, DirLightDirection.xyz); @@ -2552,7 +2661,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(VOLUMETRIC_SHADOWS) if (inWorld && !inReflection && ShadowSampling::HasDirectionalShadows()) - dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, dirVSMDetailedShadow); + dirSoftShadow = ShadowSampling::GetLightingShadow(input.WorldPosition.xyz, eyeIndex, dirVSMDetailedShadow); # endif float dirDetailedShadow = 1.0; @@ -2667,7 +2776,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) dirLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, DirLightDirection, dirLightColor, dirDetailedShadow, dirSoftShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, DirLightDirection, screenNoise, eyeIndex); dirLightContext.hairShadow = hairShadow; } # endif @@ -2695,7 +2804,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if !defined(LIGHT_LIMIT_FIX) [loop] for (uint lightIndex = 0; lightIndex < numLights; lightIndex++) { - float3 lightDirection = PointLightPosition[lightIndex].xyz - input.WorldPosition.xyz; + float3 lightDirection = PointLightPosition[eyeIndex * numLights + lightIndex].xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); float intensityFactor = saturate(lightDist / PointLightPosition[lightIndex].w); if (intensityFactor == 1) @@ -2730,7 +2839,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, lightShadow, lightShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); pointLightContext.hairShadow = hairShadow; } # endif @@ -2796,7 +2905,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) continue; } - float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -2937,7 +3046,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) pointLightContext = CreateDirectLightingContext(worldNormal.xyz, vertexNormal.xyz, viewDirection, normalizedLightDirection, lightColor, pointLightShadow, pointLightShadow); # if defined(HAIR) && defined(CS_HAIR) if (SharedData::hairSpecularSettings.Enabled) { - float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise); + float hairShadow = Hair::HairSelfShadow(input.WorldPosition.xyz, normalizedLightDirection, screenNoise, eyeIndex); pointLightContext.hairShadow = hairShadow; } # endif @@ -3024,7 +3133,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) ambientNormal = normalize(viewDirection - hairT * dot(viewDirection, hairT)); else ambientNormal = vertexNormal.xyz; - screenSpaceNormal = normalize(FrameBuffer::WorldToView(ambientNormal, false)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(ambientNormal, false, eyeIndex)); } # endif @@ -3097,7 +3206,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) lodLandDiffuseColor += directionalAmbientColor; # endif - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); # if defined(WETNESS_EFFECTS) # if !(defined(FACEGEN) || defined(FACEGEN_RGB_TINT) || defined(EYE)) || defined(TREE_ANIM) @@ -3153,6 +3262,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) color.xyz += directLightsDiffuseInput; } + // Fixes white items in UI for VR [branch] if ((PBRFlags & PBR::Flags::HasEmissive) != 0) { color.xyz += emitColor.xyz; @@ -3277,9 +3387,9 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) if (SharedData::exponentialHeightFogSettings.enabled) { float4 exponentialHeightFog; if (inReflection) { - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); } else { - exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); } fogColor = exponentialHeightFog.xyz; fogFactor = exponentialHeightFog.w; @@ -3456,7 +3566,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) indirectLobeWeights.specular += wetnessReflectance; if (waterRoughnessSpecular < 1) { // Reflection is from the water film surface; wetnessReflectance scales intensity by wetness amount. - screenSpaceNormal = normalize(FrameBuffer::WorldToView(wetnessNormal, false)); + screenSpaceNormal = normalize(FrameBuffer::WorldToView(wetnessNormal, false, eyeIndex)); material.Roughness = waterRoughnessSpecular; } # endif @@ -3464,14 +3574,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Reflectance = float4(indirectLobeWeights.specular, psout.Diffuse.w); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(screenSpaceNormal), saturate(1.0 - material.Roughness), psout.Diffuse.w); -# if defined(SNOW) -# if defined(TRUE_PBR) - psout.Parameters.x = Color::RGBToLuminanceAlternative(specularColor); - psout.Parameters.y = 0; -# else - psout.Parameters.x = Color::RGBToLuminanceAlternative(lightsSpecularColor); -# endif - psout.Parameters.w = psout.Diffuse.w; +# if defined(VR_STEREO_OPT) && !defined(SNOW) + // VR stereo reprojection: write POM depth offset to dedicated texture (u7) for StereoBlendCS. + // hasPOM disambiguates "POM ran at geometry plane (pixelOffset=0.5)" from "POM did not run". + // -1.0 is the explicit no-POM sentinel (R16_FLOAT supports negatives); StereoBlendCS checks >= 0. + PomOffsetTex[uint2(input.Position.xy)] = hasPOM ? pixelOffset : Stereo::POM_NO_DATA; # endif float masksZ = Color::RGBToYCoCg(directionalAmbientColor).x; diff --git a/package/Shaders/Particle.hlsl b/package/Shaders/Particle.hlsl index e1ac4f484f..86138906bd 100644 --- a/package/Shaders/Particle.hlsl +++ b/package/Shaders/Particle.hlsl @@ -1,6 +1,7 @@ #include "Common/Color.hlsli" #include "Common/FrameBuffer.hlsli" #include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" struct VS_INPUT { @@ -15,6 +16,9 @@ struct VS_INPUT int4 #endif TexCoord1: TEXCOORD1; +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -25,6 +29,11 @@ struct VS_OUTPUT #if defined(ENVCUBE) float4 PrecipitationOcclusionTexCoord: TEXCOORD1; #endif +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 + uint EyeIndex: EYEIDX0; +#endif // VR }; #ifdef VSHADER @@ -35,8 +44,13 @@ cbuffer PerTechnique : register(b0) cbuffer PerGeometry : register(b2) { - row_major float4x4 WorldViewProj; // 0 - row_major float4x4 WorldView; // 4 +# if !defined(VR) + row_major float4x4 WorldViewProj[1]; // 0 + row_major float4x4 WorldView[1]; // 4 +# else + row_major float4x4 WorldViewProj[2]; // 0 + row_major float4x4 WorldView[2]; // 8 +# endif # if defined(ENVCUBE) row_major float4x4 PrecipitationOcclusionWorldViewProj; // 8, 16 # endif @@ -65,6 +79,12 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif + ); + # if defined(ENVCUBE) # if defined(RAIN) float2 positionOffset = input.TexCoord1.xy; @@ -81,11 +101,11 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz = normalizedPosition * fVars2.xxx + (-(fVars2.x * 0.5).xxx + fVars1.xyz); msPosition.w = 1; - float4 viewPosition = mul(WorldViewProj, msPosition); + float4 viewPosition = mul(WorldViewProj[eyeIndex], msPosition); # if defined(RAIN) float4 adjustedMsPosition = msPosition - float4(Velocity.xyz, 0); float positionBlendParam = 0.5 * (1 + input.TexCoord1.y); - float4 adjustedViewPosition = mul(WorldViewProj, adjustedMsPosition); + float4 adjustedViewPosition = mul(WorldViewProj[eyeIndex], adjustedMsPosition); float4 finalViewPosition = lerp(adjustedViewPosition, viewPosition, positionBlendParam); # else float4 finalViewPosition = viewPosition; @@ -140,7 +160,7 @@ VS_OUTPUT main(VS_INPUT input) input.Position.xyz)); msPosition.w = 1; - float4 viewPosition = mul(WorldViewProj, msPosition); + float4 viewPosition = mul(WorldViewProj[eyeIndex], msPosition); vsout.Position.xy = positionOffset * ScaleAdjust + viewPosition.xy; vsout.Position.zw = viewPosition.zw; @@ -174,6 +194,13 @@ VS_OUTPUT main(VS_INPUT input) vsout.Color.xyz = color.xyz; # endif +# ifdef VR + vsout.EyeIndex = eyeIndex; + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); + vsout.Position = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR return vsout; } #endif @@ -230,8 +257,17 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; +# if !defined(VR) + uint eyeIndex = 0; +# else + uint eyeIndex = input.EyeIndex; +# endif // !VR + # if defined(ENVCUBE) float2 precipitationOcclusionUV = (input.PrecipitationOcclusionTexCoord.xy * 0.5 + 0.5) * TextureSize.x; +# ifdef VR + precipitationOcclusionUV *= FrameBuffer::DynamicResolutionParams1.x; // only difference in VR +# endif float precipitationOcclusion = -input.PrecipitationOcclusionTexCoord.z + TexPrecipitationOcclusionTexture.Load(float3(precipitationOcclusionUV, 0)).x; float2 underwaterMaskUv = TextureSize.yz * input.Position.xy; float underwaterMask = TexUnderwaterMask.Sample(SampUnderwaterMask, underwaterMaskUv).x; @@ -256,10 +292,10 @@ PS_OUTPUT main(PS_INPUT input) float3 propertyColor = 0.0; - float2 uv = input.Position.xy * SharedData::BufferDim.zw; + float2 uv = Stereo::ConvertFromStereoUV(input.Position.xy * SharedData::BufferDim.zw, eyeIndex); float4 positionWS = float4(2 * float2(uv.x, -uv.y + 1) - 1, input.Position.z, 1); - positionWS = mul(FrameBuffer::CameraViewProjInverse, positionWS); + positionWS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionWS); positionWS.xyz = positionWS.xyz / positionWS.w; float screenNoise = Random::InterleavedGradientNoise(input.Position.xy, SharedData::FrameCount); @@ -293,8 +329,8 @@ PS_OUTPUT main(PS_INPUT input) # if defined(LIGHT_LIMIT_FIX) uint lightCount = 0; { - float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz); - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + float3 viewPosition = FrameBuffer::WorldToView(positionWS.xyz, true, eyeIndex); + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); uint clusterIndex = 0; if (LightLimitFix::GetClusterIndex(screenUV, viewPosition.z, clusterIndex)) { @@ -307,7 +343,7 @@ PS_OUTPUT main(PS_INPUT input) if (LightLimitFix::IsLightIgnored(light) || light.lightFlags & LightLimitFix::LightFlags::Shadow) { continue; } - float3 lightDirection = light.positionWS.xyz - positionWS.xyz; + float3 lightDirection = light.positionWS[eyeIndex].xyz - positionWS.xyz; float lightDist = length(lightDirection); # if defined(ISL) diff --git a/package/Shaders/RunGrass.hlsl b/package/Shaders/RunGrass.hlsl index 07388322dd..d9edfd0aee 100644 --- a/package/Shaders/RunGrass.hlsl +++ b/package/Shaders/RunGrass.hlsl @@ -27,6 +27,9 @@ struct VS_INPUT float4 InstanceData2: TEXCOORD5; float4 InstanceData3: TEXCOORD6; float4 InstanceData4: TEXCOORD7; +#ifdef VR + uint InstanceID: SV_INSTANCEID; +#endif // VR }; #ifdef GRASS_LIGHTING @@ -36,13 +39,27 @@ struct VS_OUTPUT float4 Color: COLOR0; float VertexMult: COLOR1; float3 TexCoord: TEXCOORD0; - float3 ViewSpacePosition: TEXCOORD1; + float3 ViewSpacePosition: +# if !defined(VR) + TEXCOORD1; +# else + TEXCOORD2; +# endif # if defined(RENDER_DEPTH) - float2 Depth: TEXCOORD2; + float2 Depth: +# if !defined(VR) + TEXCOORD2; +# else + TEXCOORD3; +# endif # endif // RENDER_DEPTH float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; float4 VertexNormal: POSITION4; +# ifdef VR + float ClipDistance: SV_ClipDistance0; + float CullDistance: SV_CullDistance0; +# endif // VR }; #else struct VS_OUTPUT @@ -58,9 +75,14 @@ struct VS_OUTPUT # endif // RENDER_DEPTH float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; +# ifdef VR + float ClipDistance: SV_ClipDistance0; + float CullDistance: SV_CullDistance0; +# endif // VR }; #endif +// Constant Buffers (Flat and VR) cbuffer PerGeometry : register( #ifdef VSHADER b2 @@ -69,10 +91,11 @@ cbuffer PerGeometry : register( #endif ) { - row_major float4x4 WorldViewProj : packoffset(c0); - row_major float4x4 WorldView : packoffset(c4); - row_major float4x4 World : packoffset(c8); - row_major float4x4 PreviousWorld : packoffset(c12); +#if !defined(VR) + row_major float4x4 WorldViewProj[1] : packoffset(c0); + row_major float4x4 WorldView[1] : packoffset(c4); + row_major float4x4 World[1] : packoffset(c8); + row_major float4x4 PreviousWorld[1] : packoffset(c12); float4 FogNearColor : packoffset(c16); float3 WindVector : packoffset(c17); float WindTimer : packoffset(c17.w); @@ -84,6 +107,23 @@ cbuffer PerGeometry : register( float AlphaParam2 : packoffset(c20.w); float3 ScaleMask : packoffset(c21); float ShadowClampValue : packoffset(c21.w); +#else + row_major float4x4 WorldViewProj[2] : packoffset(c0); + row_major float4x4 WorldView[2] : packoffset(c8); + row_major float4x4 World[2] : packoffset(c16); + row_major float4x4 PreviousWorld[2] : packoffset(c24); + float4 FogNearColor : packoffset(c32); + float3 WindVector : packoffset(c33); + float WindTimer : packoffset(c33.w); + float3 DirLightDirection : packoffset(c34); + float PreviousWindTimer : packoffset(c34.w); + float3 DirLightColor : packoffset(c35); + float AlphaParam1 : packoffset(c35.w); + float3 AmbientColor : packoffset(c36); + float AlphaParam2 : packoffset(c36.w); + float3 ScaleMask : packoffset(c37); + float ShadowClampValue : packoffset(c37.w); +#endif // !VR } #ifdef VSHADER @@ -150,6 +190,11 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif // VR + ); float3x3 world3x3 = float3x3(input.InstanceData2.xyz, input.InstanceData3.xyz, float3(input.InstanceData4.x, input.InstanceData2.w, input.InstanceData3.w)); float4 msPosition = GetMSPosition(input, world3x3); @@ -165,8 +210,10 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz += windDisplacement; - float4 projSpacePosition = mul(WorldViewProj, msPosition); + float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); +# if !defined(VR) vsout.HPosition = projSpacePosition; +# endif // !VR # if defined(RENDER_DEPTH) vsout.Depth = projSpacePosition.zw; @@ -174,7 +221,11 @@ VS_OUTPUT main(VS_INPUT input) float perInstanceFade = dot(cb8[(asuint(cb7[0].x) >> 2)].xyzw, Math::IdentityMatrix[(asint(cb7[0].x) & 3)].xyzw); +# if defined(VR) + float distanceFade = 1 - saturate((length(mul(World[0], msPosition).xyz) - AlphaParam1) / AlphaParam2); +# else float distanceFade = 1 - saturate((length(projSpacePosition.xyz) - AlphaParam1) / AlphaParam2); +# endif // Note: input.Color.w is used for wind speed vsout.Color.xyz = input.Color.xyz; @@ -184,8 +235,8 @@ VS_OUTPUT main(VS_INPUT input) vsout.TexCoord.xy = input.TexCoord.xy; vsout.TexCoord.z = FogNearColor.w; - vsout.ViewSpacePosition = mul(WorldView, msPosition).xyz; - vsout.WorldPosition = mul(World, msPosition); + vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; + vsout.WorldPosition = mul(World[eyeIndex], msPosition); float4 previousMsPosition = GetMSPosition(input, world3x3); @@ -195,7 +246,13 @@ VS_OUTPUT main(VS_INPUT input) previousMsPosition.xyz += previousWindDisplacement; - vsout.PreviousWorldPosition = mul(PreviousWorld, previousMsPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); +# if defined(VR) + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); + vsout.HPosition = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // !VR // Vertex normal needs to be transformed to world-space for lighting calculations. vsout.VertexNormal.xyz = mul(world3x3, input.Normal.xyz * 2.0 - 1.0); @@ -208,6 +265,12 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif // VR + ); + float4 msPosition = GetMSPosition(input); float3 windDisplacement = CalculateWindDisplacement(input, WindTimer); @@ -221,8 +284,10 @@ VS_OUTPUT main(VS_INPUT input) msPosition.xyz += windDisplacement; - float4 projSpacePosition = mul(WorldViewProj, msPosition); + float4 projSpacePosition = mul(WorldViewProj[eyeIndex], msPosition); +# if !defined(VR) vsout.HPosition = projSpacePosition; +# endif // !VR vsout.HPosition = projSpacePosition; # if defined(RENDER_DEPTH) vsout.Depth = projSpacePosition.zw; @@ -234,7 +299,11 @@ VS_OUTPUT main(VS_INPUT input) float perInstanceFade = dot(cb8[(asuint(cb7[0].x) >> 2)].xyzw, Math::IdentityMatrix[(asint(cb7[0].x) & 3)].xyzw); +# if defined(VR) + float distanceFade = 1 - saturate((length(mul(World[0], msPosition).xyz) - AlphaParam1) / AlphaParam2); +# else float distanceFade = 1 - saturate((length(projSpacePosition.xyz) - AlphaParam1) / AlphaParam2); +# endif vsout.Color.xyz = input.Color.xyz; vsout.Color.w = distanceFade * perInstanceFade; @@ -246,10 +315,16 @@ VS_OUTPUT main(VS_INPUT input) vsout.AmbientColor.xyz = input.InstanceData1.www * (AmbientColor.xyz * input.Color.xyz); vsout.AmbientColor.w = ShadowClampValue; - vsout.ViewSpacePosition = mul(WorldView, msPosition).xyz; - vsout.WorldPosition = mul(World, msPosition); + vsout.ViewSpacePosition = mul(WorldView[eyeIndex], msPosition).xyz; + vsout.WorldPosition = mul(World[eyeIndex], msPosition); float4 previousMsPosition = GetMSPosition(input); +# if defined(VR) + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(projSpacePosition, eyeIndex); + vsout.HPosition = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // !VR # ifdef GRASS_COLLISION previousMsPosition.xyz += previousDisplacement; @@ -257,7 +332,7 @@ VS_OUTPUT main(VS_INPUT input) previousMsPosition.xyz += previousWindDisplacement; - vsout.PreviousWorldPosition = mul(PreviousWorld, previousMsPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], previousMsPosition); return vsout; } @@ -333,10 +408,12 @@ Texture2D TexSubsurfaceSampler : register(t4); # endif // GRASS_LIGHTING +# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif // !VR # if defined(SCREEN_SPACE_SHADOWS) # include "ScreenSpaceShadows/ScreenSpaceShadows.hlsli" @@ -448,13 +525,14 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float4 specColor = TexNormalSampler.SampleBias(SampNormalSampler, input.TexCoord.xy, SharedData::MipBias); # endif - psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); + psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); float3 viewDirection = -normalize(input.WorldPosition.xyz); float3 normal = normalize(input.VertexNormal.xyz); - float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); // Swaps direction of the backfaces otherwise they seem to get lit from the wrong direction. @@ -513,7 +591,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirLightColor *= ExponentialHeightFog::GetSunlightFogAttenuation(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif @@ -523,7 +601,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (!SharedData::InInterior) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); float dirDetailedShadow = 1.0; @@ -566,7 +644,11 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMSSkylight = input.WorldPosition.xyz; +# endif float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -592,7 +674,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + i]; LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; - float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -716,7 +798,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) psout.Diffuse.xyz = diffuseColor; # endif - float3 normalVS = normalize(FrameBuffer::WorldToView(normal, false)); + float3 normalVS = normalize(FrameBuffer::WorldToView(normal, false, eyeIndex)); # if defined(TRUE_PBR) psout.Albedo = float4(Color::IrradianceToGamma(indirectDiffuseLobeWeight), 1); psout.NormalGlossiness = float4(GBuffer::EncodeNormal(normalVS), 1 - pbrSurfaceProperties.Roughness, 1); @@ -754,8 +836,10 @@ PS_OUTPUT main(PS_INPUT input) if (SharedData::lodBlendingSettings.DisableTerrainVertexColors) input.Color.xyz = 1; - float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WorldPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); + + float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WorldPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); float screenNoise = Random::InterleavedGradientNoise(input.HPosition.xy, SharedData::FrameCount); float4 shadowColor = TexShadowMaskSampler.Load(int3(input.HPosition.xy, 0)); @@ -765,7 +849,7 @@ PS_OUTPUT main(PS_INPUT input) // Apply world shadow (terrain shadows, cloud shadows) directly to light color if (!SharedData::InInterior) - dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + dirLightColor *= ShadowSampling::GetWorldShadow(input.WorldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, eyeIndex); float dirDetailedShadow = 1.0; @@ -797,7 +881,7 @@ PS_OUTPUT main(PS_INPUT input) uint clusteredLightIndex = LightLimitFix::lightList[lightOffset + i]; LightLimitFix::Light light = LightLimitFix::lights[clusteredLightIndex]; - float3 lightDirection = light.positionWS.xyz - input.WorldPosition.xyz; + float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WorldPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -847,7 +931,11 @@ PS_OUTPUT main(PS_INPUT input) vertexColor /= max(vertexAO, EPSILON_DIVISION); # if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMSSkylight = input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMSSkylight = input.WorldPosition.xyz; +# endif float skylightingDiffuse = Skylighting::GetVertexSkylightingDiffuse(positionMSSkylight, normal, vertexAO); # endif // SKYLIGHTING @@ -883,8 +971,8 @@ PS_OUTPUT main(PS_INPUT input) psout.Diffuse.w = 1; - psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); - psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false)); + psout.MotionVectors = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); + psout.Normal.xy = GBuffer::EncodeNormal(FrameBuffer::WorldToView(normal, false, eyeIndex)); psout.Normal.zw = 0; psout.Albedo = float4(albedo, 1); diff --git a/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index 4d483ef0c5..1d4be8643c 100644 --- a/package/Shaders/Sky.hlsl +++ b/package/Shaders/Sky.hlsl @@ -3,6 +3,7 @@ #include "Common/Permutation.hlsli" #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" struct VS_INPUT { @@ -13,6 +14,9 @@ struct VS_INPUT #endif float4 Color: COLOR0; +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -42,23 +46,43 @@ struct VS_OUTPUT float4 WorldPosition: POSITION1; float4 PreviousWorldPosition: POSITION2; float3 FogPosition: TEXCOORD4; +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 + uint EyeIndex: EYEIDX0; +#endif // VR }; #ifdef VSHADER cbuffer PerGeometry : register(b2) { - row_major float4x4 WorldViewProj : packoffset(c0); - row_major float4x4 World : packoffset(c4); - row_major float4x4 PreviousWorld : packoffset(c8); - float3 EyePosition : packoffset(c12); +# if !defined(VR) + row_major float4x4 WorldViewProj[1] : packoffset(c0); + row_major float4x4 World[1] : packoffset(c4); + row_major float4x4 PreviousWorld[1] : packoffset(c8); + float3 EyePosition[1] : packoffset(c12); float VParams : packoffset(c12.w); float4 BlendColor[3] : packoffset(c13); float2 TexCoordOff : packoffset(c16); +# else + row_major float4x4 WorldViewProj[2] : packoffset(c0); + row_major float4x4 World[2] : packoffset(c8); + row_major float4x4 PreviousWorld[2] : packoffset(c16); + float3 EyePosition[2] : packoffset(c24); + float VParams : packoffset(c25.w); + float4 BlendColor[3] : packoffset(c26); + float2 TexCoordOff : packoffset(c29); +# endif // !VR }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif + ); float4 inputPosition = float4(input.Position.xyz, 1.0); @@ -73,8 +97,8 @@ VS_OUTPUT main(VS_INPUT input) # elif defined(HORIZFADE) - float worldHeight = mul(World, inputPosition).z; - float eyeHeightDelta = -EyePosition.z + worldHeight; + float worldHeight = mul(World[eyeIndex], inputPosition).z; + float eyeHeightDelta = -EyePosition[eyeIndex].z + worldHeight; vsout.TexCoord0.xy = input.TexCoord; vsout.TexCoord2.x = saturate((1.0 / 17.0) * eyeHeightDelta); @@ -113,11 +137,18 @@ VS_OUTPUT main(VS_INPUT input) # endif // OCCLUSION MOONMASK HORIZFADE - vsout.Position = mul(WorldViewProj, inputPosition).xyww; - vsout.WorldPosition = mul(World, inputPosition); - vsout.FogPosition = vsout.WorldPosition.xyz - EyePosition.xyz; - vsout.PreviousWorldPosition = mul(PreviousWorld, inputPosition); - + vsout.Position = mul(WorldViewProj[eyeIndex], inputPosition).xyww; + vsout.WorldPosition = mul(World[eyeIndex], inputPosition); + vsout.FogPosition = vsout.WorldPosition.xyz - EyePosition[eyeIndex].xyz; + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], inputPosition); + +# ifdef VR + vsout.EyeIndex = eyeIndex; + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.Position, eyeIndex); + vsout.Position = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR return vsout; } #endif @@ -148,10 +179,12 @@ cbuffer PerGeometry : register(b2) float2 PParams : packoffset(c0); }; +# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif # include "Common/MotionBlur.hlsli" # include "Common/SharedData.hlsli" @@ -178,6 +211,11 @@ PS_OUTPUT main(PS_INPUT input) // Color::Sky is float3->float3 (per-channel sky gamma). PParams.yyy broadcasts the packed // scalar in PParams.y to RGB; float3 matches output .xyz where skyScale is added. float3 skyScale = Color::Sky(PParams.yyy); +# if !defined(VR) + uint eyeIndex = 0; +# else + uint eyeIndex = input.EyeIndex; +# endif // !VR # ifndef OCCLUSION # ifndef TEXLERP @@ -255,12 +293,12 @@ PS_OUTPUT main(PS_INPUT input) const bool inReflection = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InReflection) != 0; if (inReflection && SharedData::exponentialHeightFogSettings.enabled) { float3 skyFogPosition = normalize(input.FogPosition.xyz) * SharedData::CameraData.x; - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(skyFogPosition, FrameBuffer::CameraPosAdjust.xyz, psout.Color.xyz, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFogNoVolumetric(skyFogPosition, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, psout.Color.xyz, float4(input.Position.xy * FrameBuffer::DynamicResolutionParams2.xy, input.Position.z, 1)); psout.Color.xyz = lerp(psout.Color.xyz, exponentialHeightFog.xyz, exponentialHeightFog.w); } # endif - float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition); + float2 screenMotionVector = MotionBlur::GetSSMotionVector(input.WorldPosition, input.PreviousWorldPosition, eyeIndex); psout.MotionVectors = float4(screenMotionVector, 0, psout.Color.w); psout.Normal = float4(0.5, 0.5, 0, psout.Color.w); diff --git a/package/Shaders/Tests/TestVR.hlsl b/package/Shaders/Tests/TestVR.hlsl new file mode 100644 index 0000000000..1d528386c9 --- /dev/null +++ b/package/Shaders/Tests/TestVR.hlsl @@ -0,0 +1,362 @@ +// HLSL Unit Tests for Common/VR.hlsli +// Tests the pure-math UV conversion functions that form the foundation of VR stereo rendering. +// These run with VR defined so the stereo code paths are exercised. +// COMPUTESHADER prevents FrameBuffer.hlsli inclusion (we only need the UV math). +#define VR +#define COMPUTESHADER +#include "/Shaders/Common/VR.hlsli" +#include "/Test/STF/ShaderTestFramework.hlsli" + +static const float kEps = 0.0001f; + +/// @tags vr, stereo, uv +/// ConvertToStereoUV: left eye maps [0,1] -> [0,0.5] +[numthreads(1, 1, 1)] void TestConvertToStereoUVLeftEye() { + float2 uv = float2(0.0, 0.5); + float2 result = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(result.x - 0.0) < kEps); + ASSERT(IsTrue, abs(result.y - 0.5) < kEps); + + uv = float2(1.0, 0.5); + result = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); + ASSERT(IsTrue, abs(result.y - 0.5) < kEps); + + uv = float2(0.5, 0.25); + result = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(result.x - 0.25) < kEps); + ASSERT(IsTrue, abs(result.y - 0.25) < kEps); +} + + /// @tags vr, stereo, uv + /// ConvertToStereoUV: right eye maps [0,1] -> [0.5,1] + [numthreads(1, 1, 1)] void TestConvertToStereoUVRightEye() +{ + float2 uv = float2(0.0, 0.5); + float2 result = Stereo::ConvertToStereoUV(uv, 1); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); + ASSERT(IsTrue, abs(result.y - 0.5) < kEps); + + uv = float2(1.0, 0.5); + result = Stereo::ConvertToStereoUV(uv, 1); + ASSERT(IsTrue, abs(result.x - 1.0) < kEps); + + uv = float2(0.5, 0.25); + result = Stereo::ConvertToStereoUV(uv, 1); + ASSERT(IsTrue, abs(result.x - 0.75) < kEps); +} + +/// @tags vr, stereo, uv +/// ConvertToStereoUV with Y inversion +[numthreads(1, 1, 1)] void TestConvertToStereoUVInvertY() { + float2 uv = float2(0.5, 0.25); + float2 result = Stereo::ConvertToStereoUV(uv, 0, 1); + ASSERT(IsTrue, abs(result.x - 0.25) < kEps); + ASSERT(IsTrue, abs(result.y - 0.75) < kEps); +} + + /// @tags vr, stereo, uv + /// ConvertFromStereoUV: left eye maps [0,0.5] -> [0,1] + [numthreads(1, 1, 1)] void TestConvertFromStereoUVLeftEye() +{ + float2 stereoUV = float2(0.0, 0.5); + float2 result = Stereo::ConvertFromStereoUV(stereoUV, 0); + ASSERT(IsTrue, abs(result.x - 0.0) < kEps); + ASSERT(IsTrue, abs(result.y - 0.5) < kEps); + + stereoUV = float2(0.5, 0.5); + result = Stereo::ConvertFromStereoUV(stereoUV, 0); + ASSERT(IsTrue, abs(result.x - 1.0) < kEps); + + stereoUV = float2(0.25, 0.25); + result = Stereo::ConvertFromStereoUV(stereoUV, 0); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); +} + +/// @tags vr, stereo, uv +/// ConvertFromStereoUV: right eye maps [0.5,1] -> [0,1] +[numthreads(1, 1, 1)] void TestConvertFromStereoUVRightEye() { + float2 stereoUV = float2(0.5, 0.5); + float2 result = Stereo::ConvertFromStereoUV(stereoUV, 1); + ASSERT(IsTrue, abs(result.x - 0.0) < kEps); + + stereoUV = float2(1.0, 0.5); + result = Stereo::ConvertFromStereoUV(stereoUV, 1); + ASSERT(IsTrue, abs(result.x - 1.0) < kEps); + + stereoUV = float2(0.75, 0.25); + result = Stereo::ConvertFromStereoUV(stereoUV, 1); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); +} + + /// @tags vr, stereo, uv + /// ConvertToStereoUV and ConvertFromStereoUV are inverses of each other + [numthreads(1, 1, 1)] void TestStereoUVRoundTrip() +{ + float2 original = float2(0.3, 0.7); + + // Left eye round-trip + float2 stereo = Stereo::ConvertToStereoUV(original, 0); + float2 recovered = Stereo::ConvertFromStereoUV(stereo, 0); + ASSERT(IsTrue, abs(recovered.x - original.x) < kEps); + ASSERT(IsTrue, abs(recovered.y - original.y) < kEps); + + // Right eye round-trip + stereo = Stereo::ConvertToStereoUV(original, 1); + recovered = Stereo::ConvertFromStereoUV(stereo, 1); + ASSERT(IsTrue, abs(recovered.x - original.x) < kEps); + ASSERT(IsTrue, abs(recovered.y - original.y) < kEps); +} + +/// @tags vr, stereo, uv +/// GetEyeIndexFromTexCoord: left half -> 0, right half -> 1 +[numthreads(1, 1, 1)] void TestGetEyeIndexFromTexCoord() { + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.0, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.25, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.49, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.5, 0.5)), 1u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.75, 0.5)), 1u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(1.0, 0.5)), 1u); +} + + /// @tags vr, stereo, uv + /// GetEyeIndexFromTexCoord is consistent with ConvertToStereoUV output + [numthreads(1, 1, 1)] void TestEyeIndexConsistentWithStereoUV() +{ + float2 monoUV = float2(0.6, 0.4); + + // Convert to stereo for left eye, then detect eye index + float2 stereoLeft = Stereo::ConvertToStereoUV(monoUV, 0); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(stereoLeft), 0u); + + // Convert to stereo for right eye, then detect eye index + float2 stereoRight = Stereo::ConvertToStereoUV(monoUV, 1); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(stereoRight), 1u); +} + +/// @tags vr, stereo, depth, edge-detection +/// MaxDepthDiff: identical neighbors -> 0 +[numthreads(1, 1, 1)] void TestMaxDepthDiffAllSame() { + float result = Stereo::MaxDepthDiff(0.5, float4(0.5, 0.5, 0.5, 0.5)); + ASSERT(IsTrue, abs(result) < kEps); +} + + /// @tags vr, stereo, depth, edge-detection + /// MaxDepthDiff: returns |center - neighbor| when one neighbor differs + [numthreads(1, 1, 1)] void TestMaxDepthDiffOneDiffers() +{ + // Only .z differs + float result = Stereo::MaxDepthDiff(0.5, float4(0.5, 0.5, 0.8, 0.5)); + ASSERT(IsTrue, abs(result - 0.3) < kEps); +} + +/// @tags vr, stereo, depth, edge-detection +/// MaxDepthDiff: returns the largest difference across all four neighbors +[numthreads(1, 1, 1)] void TestMaxDepthDiffPicksLargest() { + float result = Stereo::MaxDepthDiff(0.5, float4(0.55, 0.45, 0.9, 0.48)); + ASSERT(IsTrue, abs(result - 0.4) < kEps); // abs(0.5 - 0.9) = 0.4 +} + + /// @tags vr, stereo, depth, edge-detection + /// MaxDepthDiff: arm/world case returns exact diff (arm=0.75, world=1.0 -> 0.25) + [numthreads(1, 1, 1)] void TestMaxDepthDiffArmWorldCase() +{ + float armDepth = 0.75; + float worldDepth = 1.0; + float result = Stereo::MaxDepthDiff(armDepth, float4(worldDepth, armDepth, armDepth, armDepth)); + ASSERT(IsTrue, abs(result - abs(worldDepth - armDepth)) < kEps); +} + +/// @tags vr, stereo, depth, edge-detection +/// MaxDepthDiff: symmetric - diff(a,b) == diff(b,a) +[numthreads(1, 1, 1)] void TestMaxDepthDiffSymmetry() { + float a = 0.3, b = 0.7; + float fwd = Stereo::MaxDepthDiff(a, float4(b, a, a, a)); + float rev = Stereo::MaxDepthDiff(b, float4(a, b, b, b)); + ASSERT(IsTrue, abs(fwd - rev) < kEps); +} + + /// @tags vr, stereo, depth, edge-detection + /// MaxDepthDiff: center == 0 (mask pixel) against world neighbor + [numthreads(1, 1, 1)] void TestMaxDepthDiffMaskCenter() +{ + float result = Stereo::MaxDepthDiff(0.0, float4(0.8, 0.0, 0.0, 0.0)); + ASSERT(IsTrue, abs(result - 0.8) < kEps); +} + +/// @tags vr, stereo, edge-detection +/// ClampToEyeBounds: interior pixel is returned unchanged for both eyes +[numthreads(1, 1, 1)] void TestClampToEyeBoundsInterior() { + float2 frameDim = float2(2048, 1024); + int2 left = Stereo::ClampToEyeBounds(int2(512, 512), 0, frameDim); + ASSERT(AreEqual, left.x, 512); + ASSERT(AreEqual, left.y, 512); + + int2 right = Stereo::ClampToEyeBounds(int2(1536, 512), 1, frameDim); + ASSERT(AreEqual, right.x, 1536); + ASSERT(AreEqual, right.y, 512); +} + + /// @tags vr, stereo, edge-detection + /// ClampToEyeBounds: left eye x cannot cross the half-width seam + [numthreads(1, 1, 1)] void TestClampToEyeBoundsLeftEyeSeam() +{ + float2 frameDim = float2(2048, 1024); + // x past the seam clamps to halfWidth - 1 = 1023 + int2 result = Stereo::ClampToEyeBounds(int2(1025, 512), 0, frameDim); + ASSERT(AreEqual, result.x, 1023); +} + +/// @tags vr, stereo, edge-detection +/// ClampToEyeBounds: right eye x cannot cross the half-width seam +[numthreads(1, 1, 1)] void TestClampToEyeBoundsRightEyeSeam() { + float2 frameDim = float2(2048, 1024); + // x before the seam clamps to halfWidth = 1024 + int2 result = Stereo::ClampToEyeBounds(int2(1022, 512), 1, frameDim); + ASSERT(AreEqual, result.x, 1024); +} + + /// @tags vr, stereo, edge-detection + /// ClampToEyeBounds: x clamped at outer borders (left eye left edge, right eye right edge) + [numthreads(1, 1, 1)] void TestClampToEyeBoundsOuterBorders() +{ + float2 frameDim = float2(2048, 1024); + int2 leftBorder = Stereo::ClampToEyeBounds(int2(-1, 512), 0, frameDim); + ASSERT(AreEqual, leftBorder.x, 0); + + int2 rightBorder = Stereo::ClampToEyeBounds(int2(2049, 512), 1, frameDim); + ASSERT(AreEqual, rightBorder.x, 2047); +} + +/// @tags vr, stereo, edge-detection +/// ClampToEyeBounds: y is clamped to [0, frameDim.y - 1] independently of eye +[numthreads(1, 1, 1)] void TestClampToEyeBoundsY() { + float2 frameDim = float2(2048, 1024); + int2 top = Stereo::ClampToEyeBounds(int2(512, -1), 0, frameDim); + ASSERT(AreEqual, top.y, 0); + + int2 bottom = Stereo::ClampToEyeBounds(int2(512, 1025), 0, frameDim); + ASSERT(AreEqual, bottom.y, 1023); +} + + /// @tags vr, stereo, edge-detection + /// ClampToEyeUV: interior UV is returned unchanged for both eyes + [numthreads(1, 1, 1)] void TestClampToEyeUVInterior() +{ + float2 left = Stereo::ClampToEyeUV(float2(0.25, 0.5), 0); + ASSERT(IsTrue, abs(left.x - 0.25) < kEps); + ASSERT(IsTrue, abs(left.y - 0.5) < kEps); + + float2 right = Stereo::ClampToEyeUV(float2(0.75, 0.5), 1); + ASSERT(IsTrue, abs(right.x - 0.75) < kEps); + ASSERT(IsTrue, abs(right.y - 0.5) < kEps); +} + +/// @tags vr, stereo, edge-detection +/// ClampToEyeUV: left eye x cannot cross the x=0.5 seam +[numthreads(1, 1, 1)] void TestClampToEyeUVLeftEyeSeam() { + // x past the seam clamps to 0.5 + float2 result = Stereo::ClampToEyeUV(float2(0.6, 0.5), 0); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); +} + + /// @tags vr, stereo, edge-detection + /// ClampToEyeUV: right eye x cannot cross the x=0.5 seam + [numthreads(1, 1, 1)] void TestClampToEyeUVRightEyeSeam() +{ + // x before the seam clamps to 0.5 + float2 result = Stereo::ClampToEyeUV(float2(0.4, 0.5), 1); + ASSERT(IsTrue, abs(result.x - 0.5) < kEps); +} + +/// @tags vr, stereo, edge-detection +/// ClampToEyeUV: x clamped at outer borders (left eye at 0.0, right eye at 1.0) +[numthreads(1, 1, 1)] void TestClampToEyeUVOuterBorders() { + float2 leftBorder = Stereo::ClampToEyeUV(float2(-0.1, 0.5), 0); + ASSERT(IsTrue, abs(leftBorder.x - 0.0) < kEps); + + float2 rightBorder = Stereo::ClampToEyeUV(float2(1.1, 0.5), 1); + ASSERT(IsTrue, abs(rightBorder.x - 1.0) < kEps); +} + + /// @tags vr, stereo, edge-detection + /// ClampToEyeUV: y coordinate is not modified + [numthreads(1, 1, 1)] void TestClampToEyeUVYUnchanged() +{ + float2 result = Stereo::ClampToEyeUV(float2(0.25, 1.5), 0); + ASSERT(IsTrue, abs(result.y - 1.5) < kEps); + + result = Stereo::ClampToEyeUV(float2(0.75, -0.5), 1); + ASSERT(IsTrue, abs(result.y - (-0.5)) < kEps); +} + +/// @tags vr, stereo, uv +/// ConvertToStereoUV clamps input x to [0,1] via saturate +[numthreads(1, 1, 1)] void TestConvertToStereoUVClamping() { + // x > 1 should be clamped to 1 before conversion + float2 uv = float2(1.5, 0.5); + float2 resultLeft = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(resultLeft.x - 0.5) < kEps); // saturate(1.5)=1.0, (1.0+0)/2=0.5 + + float2 resultRight = Stereo::ConvertToStereoUV(uv, 1); + ASSERT(IsTrue, abs(resultRight.x - 1.0) < kEps); // saturate(1.5)=1.0, (1.0+1)/2=1.0 + + // x < 0 should be clamped to 0 + uv = float2(-0.5, 0.5); + resultLeft = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(resultLeft.x - 0.0) < kEps); // saturate(-0.5)=0.0, (0+0)/2=0 +} + + /// @tags vr, stereo, uv + /// ConvertUVToNormalizedScreenSpace maps to [-1,1] range + [numthreads(1, 1, 1)] void TestConvertUVToNormalizedScreenSpace() +{ + // Center of left eye (stereo UV 0.25) -> x should be near 0 (center of that eye) + float2 result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 0.5)); + ASSERT(IsTrue, abs(result.x - 0.0) < kEps); + ASSERT(IsTrue, abs(result.y - 0.0) < kEps); + + // Center of right eye (stereo UV 0.75) -> x should also be near 0 + result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.75, 0.5)); + ASSERT(IsTrue, abs(result.x - 0.0) < kEps); + + // Outer edges (stereo UV 0.0 and 1.0) -> x = +1 + result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.0, 0.5)); + ASSERT(IsTrue, abs(result.x - 1.0) < kEps); + + // Inner edge / midpoint (stereo UV 0.5) -> x = -1 + result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.5, 0.5)); + ASSERT(IsTrue, abs(result.x - (-1.0)) < kEps); + + // Top -> y = -1, bottom -> y = 1 + result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 0.0)); + ASSERT(IsTrue, abs(result.y - (-1.0)) < kEps); + + result = Stereo::ConvertUVToNormalizedScreenSpace(float2(0.25, 1.0)); + ASSERT(IsTrue, abs(result.y - 1.0) < kEps); +} + +/// @tags vr, stereo, uv +/// ApplyVelocityToUV: correctly translates UV keeping stereoscopic boundaries intact and reports bounds +[numthreads(1, 1, 1)] void TestApplyVelocityToUV() { + float2 velocity = float2(0.1, 0.0); + bool oob; + + // Left eye bounds [0, 0.5], mapping left eye UV of 0.25 + 0.1 mono velocity -> 0.3 stereo + float2 resultLeft = Stereo::ApplyVelocityToUV(float2(0.25, 0.5), velocity, oob); + ASSERT(IsTrue, abs(resultLeft.x - 0.3) < kEps); + ASSERT(IsTrue, abs(resultLeft.y - 0.5) < kEps); + ASSERT(IsFalse, oob); + + // Right eye bounds [0.5, 1.0], mapping right eye UV of 0.75 + 0.1 mono velocity -> 0.8 stereo + float2 resultRight = Stereo::ApplyVelocityToUV(float2(0.75, 0.5), velocity, oob); + ASSERT(IsTrue, abs(resultRight.x - 0.8) < kEps); + ASSERT(IsTrue, abs(resultRight.y - 0.5) < kEps); + ASSERT(IsFalse, oob); + + // OOB condition: mono velocity pushes past 1.0 + float2 resultOob = Stereo::ApplyVelocityToUV(float2(0.25, 0.5), float2(1.5, 0.0), oob); + ASSERT(IsTrue, oob); + // In VR, out of bounds is clamped (mono x < 0 maps to 0 -> stereo 0, mono x > 1 saturates to 1 -> stereo 0.5 for left) + ASSERT(IsTrue, abs(resultOob.x - 0.5) < kEps); +} diff --git a/package/Shaders/Tests/TestVRFlat.hlsl b/package/Shaders/Tests/TestVRFlat.hlsl new file mode 100644 index 0000000000..7f1e1fd978 --- /dev/null +++ b/package/Shaders/Tests/TestVRFlat.hlsl @@ -0,0 +1,90 @@ +// HLSL Unit Tests for Common/VR.hlsli - Flat (non-VR) mode +// Verifies that all Stereo:: functions are correct no-ops / identity when VR is not defined. +// This is the code path most developers exercise. +#define COMPUTESHADER +#include "/Shaders/Common/VR.hlsli" +#include "/Test/STF/ShaderTestFramework.hlsli" + +static const float kEps = 0.0001f; + +/// @tags vr, flat, uv +/// ConvertToStereoUV is identity in flat mode +[numthreads(1, 1, 1)] void TestFlatConvertToStereoUVIsIdentity() { + float2 uv = float2(0.3, 0.7); + float2 result = Stereo::ConvertToStereoUV(uv, 0); + ASSERT(IsTrue, abs(result.x - uv.x) < kEps); + ASSERT(IsTrue, abs(result.y - uv.y) < kEps); + + // Eye index should not matter in flat + result = Stereo::ConvertToStereoUV(uv, 1); + ASSERT(IsTrue, abs(result.x - uv.x) < kEps); + ASSERT(IsTrue, abs(result.y - uv.y) < kEps); + + // invertY param should also be ignored + result = Stereo::ConvertToStereoUV(uv, 0, 1); + ASSERT(IsTrue, abs(result.x - uv.x) < kEps); + ASSERT(IsTrue, abs(result.y - uv.y) < kEps); +} + + /// @tags vr, flat, uv + /// ConvertFromStereoUV is identity in flat mode + [numthreads(1, 1, 1)] void TestFlatConvertFromStereoUVIsIdentity() +{ + float2 uv = float2(0.6, 0.4); + float2 result = Stereo::ConvertFromStereoUV(uv, 0); + ASSERT(IsTrue, abs(result.x - uv.x) < kEps); + ASSERT(IsTrue, abs(result.y - uv.y) < kEps); + + result = Stereo::ConvertFromStereoUV(uv, 1); + ASSERT(IsTrue, abs(result.x - uv.x) < kEps); + ASSERT(IsTrue, abs(result.y - uv.y) < kEps); +} + +/// @tags vr, flat, uv +/// float3/float4 overloads are also identity in flat mode +[numthreads(1, 1, 1)] void TestFlatStereoUVOverloadsAreIdentity() { + float3 uv3 = float3(0.3, 0.7, 0.5); + float3 result3 = Stereo::ConvertToStereoUV(uv3, 0); + ASSERT(IsTrue, abs(result3.x - uv3.x) < kEps); + ASSERT(IsTrue, abs(result3.y - uv3.y) < kEps); + ASSERT(IsTrue, abs(result3.z - uv3.z) < kEps); + + result3 = Stereo::ConvertFromStereoUV(uv3, 1); + ASSERT(IsTrue, abs(result3.x - uv3.x) < kEps); + ASSERT(IsTrue, abs(result3.y - uv3.y) < kEps); + ASSERT(IsTrue, abs(result3.z - uv3.z) < kEps); + + float4 uv4 = float4(0.3, 0.7, 0.5, 1.0); + float4 result4 = Stereo::ConvertToStereoUV(uv4, 0); + ASSERT(IsTrue, abs(result4.x - uv4.x) < kEps); + ASSERT(IsTrue, abs(result4.y - uv4.y) < kEps); + ASSERT(IsTrue, abs(result4.z - uv4.z) < kEps); + ASSERT(IsTrue, abs(result4.w - uv4.w) < kEps); + + float4 result4_from = Stereo::ConvertFromStereoUV(uv4, 1); + ASSERT(IsTrue, abs(result4_from.x - uv4.x) < kEps); + ASSERT(IsTrue, abs(result4_from.y - uv4.y) < kEps); + ASSERT(IsTrue, abs(result4_from.z - uv4.z) < kEps); + ASSERT(IsTrue, abs(result4_from.w - uv4.w) < kEps); +} + + /// @tags vr, flat, uv + /// Round-trip through To/From is identity in flat mode + [numthreads(1, 1, 1)] void TestFlatStereoUVRoundTrip() +{ + float2 uv = float2(0.8, 0.2); + float2 stereo = Stereo::ConvertToStereoUV(uv, 0); + float2 recovered = Stereo::ConvertFromStereoUV(stereo, 0); + ASSERT(IsTrue, abs(recovered.x - uv.x) < kEps); + ASSERT(IsTrue, abs(recovered.y - uv.y) < kEps); +} + +/// @tags vr, flat, uv +/// GetEyeIndexFromTexCoord always returns 0 in flat mode +[numthreads(1, 1, 1)] void TestFlatGetEyeIndexAlwaysZero() { + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.0, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.25, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.5, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(0.75, 0.5)), 0u); + ASSERT(AreEqual, Stereo::GetEyeIndexFromTexCoord(float2(1.0, 0.5)), 0u); +} diff --git a/package/Shaders/Utility.hlsl b/package/Shaders/Utility.hlsl index d5335d0934..356ab59b07 100644 --- a/package/Shaders/Utility.hlsl +++ b/package/Shaders/Utility.hlsl @@ -4,6 +4,8 @@ #include "Common/Random.hlsli" #include "Common/SharedData.hlsli" #include "Common/Skinned.hlsli" +#include "Common/VR.hlsli" + #if defined(RENDER_SHADOWMASK) || defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) # define RENDER_SHADOWMASK_ANY #endif @@ -27,6 +29,9 @@ struct VS_INPUT float4 BoneWeights: BLENDWEIGHT0; float4 BoneIndices: BLENDINDICES0; #endif +#if defined(VR) + uint InstanceID: SV_INSTANCEID; +#endif // VR }; struct VS_OUTPUT @@ -68,13 +73,23 @@ struct VS_OUTPUT float Depth: TEXCOORD2; # endif #endif +#if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 + uint EyeIndex: EYEIDX0; +#endif // VR }; #ifdef VSHADER cbuffer PerTechnique : register(b0) { - float4 HighDetailRange : packoffset(c0); // loaded cells center in xy, size in zw +# if !defined(VR) + float4 HighDetailRange[1] : packoffset(c0); // loaded cells center in xy, size in zw float2 ParabolaParam : packoffset(c1); // inverse radius in x, y is 1 for forward hemisphere or -1 for backward hemisphere +# else + float4 HighDetailRange[2] : packoffset(c0); // loaded cells center in xy, size in zw + float2 ParabolaParam : packoffset(c2); // inverse radius in x, y is 1 for forward hemisphere or -1 for backward hemisphere +# endif // VR }; cbuffer PerMaterial : register(b1) @@ -84,11 +99,19 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { +# if !defined(VR) float4 ShadowFadeParam : packoffset(c0); - row_major float4x4 World : packoffset(c1); - float4 EyePos : packoffset(c5); + row_major float4x4 World[1] : packoffset(c1); + float4 EyePos[1] : packoffset(c5); float4 WaterParams : packoffset(c6); float4 TreeParams : packoffset(c7); +# else + float4 ShadowFadeParam : packoffset(c0); + row_major float4x4 World[2] : packoffset(c1); + float4 EyePos[2] : packoffset(c9); + float4 WaterParams : packoffset(c11); + float4 TreeParams : packoffset(c12); +# endif // VR }; float2 SmoothSaturate(float2 value) @@ -100,12 +123,18 @@ VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif + ); + # if (defined(RENDER_DEPTH) && defined(RENDER_SHADOWMASK_ANY)) || SHADOWFILTER == 2 vsout.PositionCS.xy = input.PositionMS.xy; # if defined(RENDER_SHADOWMASKDPB) || defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) vsout.PositionCS.z = ShadowFadeParam.z; # else - vsout.PositionCS.z = HighDetailRange.x; + vsout.PositionCS.z = HighDetailRange[eyeIndex].x; # endif vsout.PositionCS.w = 1; # elif defined(STENCIL_ABOVE_WATER) @@ -128,18 +157,18 @@ VS_OUTPUT main(VS_INPUT input) # endif # if defined(LOD_LANDSCAPE) - positionMS = LodLandscape::AdjustLodLandscapeVertexPositionMS(positionMS, World, HighDetailRange); + positionMS = LodLandscape::AdjustLodLandscapeVertexPositionMS(positionMS, World[eyeIndex], HighDetailRange[eyeIndex]); # endif # if defined(SKINNED) precise int4 boneIndices = 765.01.xxxx * input.BoneIndices.xyzw; - float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, boneIndices, FrameBuffer::CameraPosAdjust.xyz, input.BoneWeights); + float3x4 worldMatrix = Skinned::GetBoneTransformMatrix(Bones, boneIndices, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, input.BoneWeights); precise float4 positionWS = float4(mul(positionMS, transpose(worldMatrix)), 1); - positionCS = mul(FrameBuffer::CameraViewProj, positionWS); + positionCS = mul(FrameBuffer::CameraViewProj[eyeIndex], positionWS); # else - precise float4x4 modelViewProj = mul(FrameBuffer::CameraViewProj, World); + precise float4x4 modelViewProj = mul(FrameBuffer::CameraViewProj[eyeIndex], World[eyeIndex]); positionCS = mul(modelViewProj, positionMS); # endif @@ -164,9 +193,9 @@ VS_OUTPUT main(VS_INPUT input) # if defined(SKINNED) float3x3 boneRSMatrix = Skinned::GetBoneRSMatrix(Bones, boneIndices, input.BoneWeights); normalMS = normalize(mul(normalMS, transpose(boneRSMatrix))); - normalVS = mul(FrameBuffer::CameraView, float4(normalMS, 0)).xyz; + normalVS = mul(FrameBuffer::CameraView[eyeIndex], float4(normalMS, 0)).xyz; # else - normalVS = mul(mul(FrameBuffer::CameraView, World), float4(normalMS, 0)).xyz; + normalVS = mul(mul(FrameBuffer::CameraView[eyeIndex], World[eyeIndex]), float4(normalMS, 0)).xyz; # endif # if defined(RENDER_NORMAL_CLAMP) normalVS = max(min(normalVS, 0.1), -0.1); @@ -191,12 +220,12 @@ VS_OUTPUT main(VS_INPUT input) float falloff = 1; # if defined(RENDER_NORMAL_FALLOFF) # if defined(SKINNED) - falloff = dot(normalMS, normalize(EyePos.xyz - positionWS.xyz)); + falloff = dot(normalMS, normalize(EyePos[eyeIndex].xyz - positionWS.xyz)); # else - falloff = dot(normalMS, normalize(EyePos.xyz - positionMS.xyz)); + falloff = dot(normalMS, normalize(EyePos[eyeIndex].xyz - positionMS.xyz)); # endif # endif - texCoord.w = EyePos.w * falloff; + texCoord.w = EyePos[eyeIndex].w * falloff; # endif vsout.TexCoord0 = texCoord; @@ -244,6 +273,13 @@ VS_OUTPUT main(VS_INPUT input) vsout.PositionCS.z += 5.0; # endif +# ifdef VR + vsout.EyeIndex = eyeIndex; + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.PositionCS, eyeIndex); + vsout.PositionCS = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR return vsout; } #endif @@ -296,18 +332,37 @@ cbuffer PerGeometry : register(b2) float4 PropertyColor : packoffset(c1); float4 AlphaTestRef : packoffset(c2); float4 ShadowLightParam : packoffset(c3); // Falloff in x, ShadowDistance squared in z +# if !defined(VR) float4x3 FocusShadowMapProj[4] : packoffset(c4); -# if defined(RENDER_SHADOWMASK) - float4x3 ShadowMapProj[3] : packoffset(c16); // 16, 19, 22 -# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) - float4x4 ShadowMapProj[3] : packoffset(c16); -# endif +# if defined(RENDER_SHADOWMASK) + float4x3 ShadowMapProj[1][3] : packoffset(c16); // 16, 19, 22 +# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) + float4x4 ShadowMapProj[1][3] : packoffset(c16); +# endif +# else + float4 VRUnknown : packoffset(c4); // used to multiply by identity matrix, see e.g., 4202499.ps.bin.hlsl + /* + r1.x = dot(cb2[4].xz, icb[r0.w+0].xz); + r1.x = r0.x * cb12[86].x + -r1.x; + r0.w = (int)r0.w + 1; + r0.w = (int)r0.w + -1; + r0.w = dot(cb2[4].yw, icb[r0.w+0].xz); + */ + float4x3 FocusShadowMapProj[4] : packoffset(c5); +# if defined(RENDER_SHADOWMASK) + float4x3 ShadowMapProj[2][3] : packoffset(c29); // VR has a couple of offsets of 3, e.g., {29, 32, 35} and {38, 41, 44}, compare to Flat which does [16, 19, 22] +# elif defined(RENDER_SHADOWMASKSPOT) || defined(RENDER_SHADOWMASKPB) || defined(RENDER_SHADOWMASKDPB) + float4x4 ShadowMapProj[2][3] : packoffset(c29); +# endif +# endif // VR } +# if !defined(VR) cbuffer AlphaTestRefCB : register(b11) { float AlphaTestRefRS : packoffset(c0); } +# endif // !VR float SampleShadowPCF(Texture2DArray tex, SamplerComparisonState samp, float2 baseUV, float layerIndex, float compareValue, float2x2 rotationMatrix, float radius) { @@ -337,6 +392,11 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; +# if !defined(VR) + uint eyeIndex = 0; +# else + uint eyeIndex = input.EyeIndex; +# endif // !VR # if defined(ADDITIONAL_ALPHA_MASK) uint2 alphaMask = input.PositionCS.xy; alphaMask.x = ((alphaMask.x << 2) & 12); @@ -428,9 +488,9 @@ PS_OUTPUT main(PS_INPUT input) TexStencilSampler.GetDimensions(0, stencilDimensions.x, stencilDimensions.y, stencilDimensions.z); stencilValue = TexStencilSampler.Load(float3(stencilDimensions.xy * depthUv, 0)).x; # endif - depthUv = depthUv * FrameBuffer::DynamicResolutionParams2.xy; + depthUv = Stereo::ConvertFromStereoUV(depthUv * FrameBuffer::DynamicResolutionParams2.xy, eyeIndex); float4 positionCS = float4(2 * float2(depthUv.x, -depthUv.y + 1) - 1, depth, 1); - float4 positionMS = mul(FrameBuffer::CameraViewProjInverse, positionCS); + float4 positionMS = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], positionCS); positionMS.xyz = positionMS.xyz / positionMS.w; float fadeFactor = 1 - pow(saturate(dot(positionMS.xyz, positionMS.xyz) / ShadowLightParam.z), 8); @@ -453,15 +513,15 @@ PS_OUTPUT main(PS_INPUT input) shadowColor = float4(0, 0, 0, 0); if (EndSplitDistances.z >= shadowMapDepth) { - float4x3 lightProjectionMatrix = ShadowMapProj[0]; + float4x3 lightProjectionMatrix = ShadowMapProj[eyeIndex][0]; float shadowMapThreshold = AlphaTestRef.y; float cascadeIndex = 0; if (2.5 < EndSplitDistances.w && EndSplitDistances.y < shadowMapDepth) { - lightProjectionMatrix = ShadowMapProj[2]; + lightProjectionMatrix = ShadowMapProj[eyeIndex][2]; shadowMapThreshold = AlphaTestRef.z; cascadeIndex = 2; } else if (EndSplitDistances.x < shadowMapDepth) { - lightProjectionMatrix = ShadowMapProj[1]; + lightProjectionMatrix = ShadowMapProj[eyeIndex][1]; shadowMapThreshold = AlphaTestRef.z; cascadeIndex = 1; } @@ -484,7 +544,7 @@ PS_OUTPUT main(PS_INPUT input) if (cascadeIndex < 1 && StartSplitDistances.y < shadowMapDepth) { float cascade1ShadowVisibility = 0; - float3 cascade1PositionLS = mul(transpose(ShadowMapProj[1]), float4(positionMS.xyz, 1)).xyz; + float3 cascade1PositionLS = mul(transpose(ShadowMapProj[eyeIndex][1]), float4(positionMS.xyz, 1)).xyz; # if SHADOWFILTER == 0 float cascade1ShadowMapValue = TexShadowMapSampler.Sample(SampShadowMapSampler, float3(cascade1PositionLS.xy, 1)).x; @@ -520,7 +580,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = lerp(1.0 * !SharedData::InInterior, shadowVisibility, fadeFactor); } # elif defined(RENDER_SHADOWMASKSPOT) - float4 positionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)); + float4 positionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)); positionLS.xyz /= positionLS.w; float2 shadowMapUv = positionLS.xy * 0.5 + 0.5; float shadowBaseVisibility = 0; @@ -558,7 +618,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = fadeFactor * shadowVisibility; # elif defined(RENDER_SHADOWMASKPB) - float4 unadjustedPositionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)); + float4 unadjustedPositionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)); float shadowVisibility = 0; @@ -583,7 +643,7 @@ PS_OUTPUT main(PS_INPUT input) shadowColor.xyzw = fadeFactor * shadowVisibility; # elif defined(RENDER_SHADOWMASKDPB) - float3 positionLS = mul(transpose(ShadowMapProj[0]), float4(positionMS.xyz, 1)).xyz; + float3 positionLS = mul(transpose(ShadowMapProj[eyeIndex][0]), float4(positionMS.xyz, 1)).xyz; bool lowerHalf = positionLS.z * 0.5 + 0.5 < 0; float3 normalizedPositionLS = normalize(positionLS); diff --git a/package/Shaders/VR/InSceneOverlay.ps.hlsl b/package/Shaders/VR/InSceneOverlay.ps.hlsl new file mode 100644 index 0000000000..7638542c53 --- /dev/null +++ b/package/Shaders/VR/InSceneOverlay.ps.hlsl @@ -0,0 +1,18 @@ +// VR In-Scene Overlay Pixel Shader +// Samples overlay texture with alpha blending support + +Texture2D shaderTexture : register(t0); +SamplerState sampleType : register(s0); + +struct PS_INPUT +{ + float4 pos: SV_POSITION; + float2 uv: TEXCOORD0; +}; + +float4 main(PS_INPUT input) : + SV_TARGET +{ + float4 color = shaderTexture.Sample(sampleType, input.uv); + return color; +} diff --git a/package/Shaders/VR/InSceneOverlay.vs.hlsl b/package/Shaders/VR/InSceneOverlay.vs.hlsl new file mode 100644 index 0000000000..b8d2d09308 --- /dev/null +++ b/package/Shaders/VR/InSceneOverlay.vs.hlsl @@ -0,0 +1,27 @@ +// VR In-Scene Overlay Vertex Shader +// Simple pass-through shader for rendering overlay quad in VR + +cbuffer MatrixBuffer : register(b0) +{ + matrix wvp; +}; + +struct VS_INPUT +{ + float3 pos: POSITION; + float2 uv: TEXCOORD0; +}; + +struct PS_INPUT +{ + float4 pos: SV_POSITION; + float2 uv: TEXCOORD0; +}; + +PS_INPUT main(VS_INPUT input) +{ + PS_INPUT output; + output.pos = mul(float4(input.pos, 1.0f), wvp); + output.uv = input.uv; + return output; +} diff --git a/package/Shaders/VR/StereoBlendCS.hlsl b/package/Shaders/VR/StereoBlendCS.hlsl new file mode 100644 index 0000000000..a6e66d1ea0 --- /dev/null +++ b/package/Shaders/VR/StereoBlendCS.hlsl @@ -0,0 +1,362 @@ +// Stereo Bilateral Blend - Post-composite stereo consistency pass for VR +// +// Full-image depth-aware bilateral blend with back-check validation that +// reprojects each pixel to the other eye and blends based on depth agreement. +// Source and destination edge detection guard silhouette boundaries before +// reprojection; the back-check provides a second layer of validation. +// +// Based on the stereo-aware bilateral filter from: +// Shi, Billeter, Eisemann 2022, "Stereo-consistent screen-space ambient occlusion" +// https://eprints.whiterose.ac.uk/id/eprint/187713/ + +#include "Common/Color.hlsli" +#include "Common/FrameBuffer.hlsli" +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" + +Texture2D ColorTexture : register(t0); +Texture2D DepthTexture : register(t1); + +RWTexture2D OutputRW : register(u0); + +#ifdef STEREO_OVERWRITE +RWTexture2D MotionRW : register(u1); +Texture2D ModeTexture : register(t2); +Texture2D PomOffsetTexture : register(t3); // R16_FLOAT: Stereo::POM_NO_DATA (-1.0) = no POM; >= 0.0 = POM ran +SamplerState LinearSampler : register(s0); + +# include "VRStereoOptimizations/modes.hlsli" + +// Hardware bilinear color sample from reprojected pixel coordinates. +// Converts integer pixel coords to proper full-texture UV for SampleLevel, +// clamped to the active DRS viewport to prevent sampling stale data. +// Motion vectors stay as integer Load() — filtering them breaks DLSS. +float4 SampleReprojectedColor(float2 stereoUV, float2 frameDim) +{ + uint texW, texH; + ColorTexture.GetDimensions(texW, texH); + float2 texSize = float2(texW, texH); + float2 minUV = 0.5 / texSize; + float2 maxUV = (frameDim - 0.5) / texSize; + stereoUV = clamp(stereoUV, minUV, maxUV); + return ColorTexture.SampleLevel(LinearSampler, stereoUV, 0); +} +#endif + +cbuffer StereoBlendCB : register(b1) +{ + float2 FrameDim; + float2 RcpFrameDim; + float DepthSigma; + float MaxBlendFactor; + float ColorDiffThreshold; + float DebugEdgeTint; + uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer, 3 = POM depth heatmap + float FullBlendDistance; + float POMDepthScale; + float _pad; +}; + +static const float kEdgeDepthThreshold = 0.05; // NDC depth difference above which a pixel is considered a depth discontinuity and excluded from stereo blend +static const int kEdgeMargin = 2; // Neighbor offset (pixels) for destination edge + mask boundary check +static const float kDepthAgreementThreshold = 0.015; // Relative depth difference threshold for overwrite mode disocclusion rejection + +// Samples four depth neighbors in a cross pattern (±offset pixels) around center, +// clamped to eyeIndex's half of the packed stereo buffer to avoid seam contamination. +float4 SampleCrossDepths(int2 center, int offset, uint eyeIndex) +{ + return float4( + DepthTexture[Stereo::ClampToEyeBounds(center + int2(offset, 0), eyeIndex, FrameDim)], + DepthTexture[Stereo::ClampToEyeBounds(center + int2(-offset, 0), eyeIndex, FrameDim)], + DepthTexture[Stereo::ClampToEyeBounds(center + int2(0, offset), eyeIndex, FrameDim)], + DepthTexture[Stereo::ClampToEyeBounds(center + int2(0, -offset), eyeIndex, FrameDim)]); +} + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + +#ifdef STEREO_OVERWRITE + // ========================================================================= + // Mode-driven stereo merge: reads per-pixel classification from StencilCS + // and applies appropriate action per mode and eye. + // Mode texture is full SBS resolution — ModeTexture[dtid] maps directly. + // ========================================================================= + + float2 uv = (dtid + 0.5) * RcpFrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float centerDepth = DepthTexture[dtid]; + + // HMD mask pixels (depth >= 1.0 in reversed-Z) — always skip + if (centerDepth >= 1.0) + return; + + uint pixelMode = ModeTexture[dtid]; + + // Debug mode 1: depth map diagnostic — show mode texture as solid colors (all pixels) + if (DebugMode == 1) { + float4 c = ColorTexture[dtid]; + if (pixelMode == MODE_EDGE) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); + else if (pixelMode == MODE_EDGE_NEIGHBOUR) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); + else if (pixelMode == MODE_DISOCCLUDED) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); + else if (pixelMode == MODE_FULL_BLEND) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); + return; + } + + // Debug mode 2: full blend depth visualizer — cyan tint based on proximity to FullBlendDistance + if (DebugMode == 2) { + if (centerDepth < 1e-5 || centerDepth >= 1.0) + return; + float linDepth = SharedData::GetScreenDepth(centerDepth); + if (linDepth < FullBlendDistance) { + float4 c = ColorTexture[dtid]; + float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); + } + return; + } + + // Debug mode 3: POM depth data visualizer — show PomOffsetTexture as color + if (DebugMode == 3) { + float pomVal = PomOffsetTexture[dtid]; + float4 c = ColorTexture[dtid]; + if (pomVal >= 0.0) { + // POM pixel: red-to-green gradient based on parallaxAmount + // Red = peak (high pomVal, closer to camera), Green = valley (low pomVal, farther), Yellow = geometry plane + float3 pomColor = float3(pomVal, 1.0 - pomVal, 0); + OutputRW[dtid] = float4(lerp(c.rgb, pomColor, 0.7), c.a); + } + // Non-POM pixels store -1.0 sentinel, left untouched + return; + } + + // MODE_DISOCCLUDED: fully shaded, leave untouched + if (pixelMode == MODE_DISOCCLUDED) + return; + + // MODE_FULL_BLEND: bilateral blend for 2x supersampling + if (pixelMode == MODE_FULL_BLEND) { + float4 center = ColorTexture[dtid]; + + // Check for POM depth offset at this pixel + // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. + // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). + // Correction: high pomVal should push depth closer (smaller linear depth), + // so we use (0.5 - pomOffset) to get a negative correction for peaks. + // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. + float reprojDepthFB = centerDepth; + float pomOffsetFB = PomOffsetTexture[dtid]; + if (pomOffsetFB >= 0.0 && POMDepthScale > 0) { + float linDepthFB = SharedData::GetScreenDepth(centerDepth); + float depthCorrectionFB = (0.5 - pomOffsetFB) * POMDepthScale; + float newLinDepthFB = max(linDepthFB + depthCorrectionFB, 1e-4); + reprojDepthFB = (SharedData::CameraData.x - SharedData::CameraData.w / newLinDepthFB) / SharedData::CameraData.z; + } + + // Reproject to the other eye + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepthFB, eyeIndex, FrameDim); + if (!r.valid) { + // Debug tint for failed reprojection + if (DebugEdgeTint > 0) + OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); + return; + } + + // Only blend with pixels that have valid composited data in both eyes + uint otherMode = ModeTexture[r.otherPx]; + if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) + return; + + float4 otherColor = SampleReprojectedColor(r.otherStereoUV, FrameDim); + float otherDepth = DepthTexture[r.otherPx]; + + // Depth-weighted bilateral blend + float maxDepth = max(max(centerDepth, otherDepth), 1e-5); + float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); + float blendWeight = 0.5 * depthAgreement; + + float4 result = lerp(center, otherColor, blendWeight); + + if (DebugEdgeTint > 0) + result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); + + OutputRW[dtid] = result; + return; + } + + if (eyeIndex == 0) { + // Eye 0 (left eye): fully shaded for all modes — only apply debug tint to edge pixels + if (DebugEdgeTint > 0 && pixelMode == MODE_EDGE) { + float4 c = ColorTexture[dtid]; + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), DebugEdgeTint), c.a); + } + return; + } + + // Eye 1 (right eye): reproject all non-disoccluded, non-full-blend pixels + // (MAIN, EDGE) from Eye 0 (left eye). In VR stereo rendering, Eye 0 is + // fully shaded; Eye 1 pixels marked as reprojectable by StencilCS are + // filled with reprojected color from Eye 0 to save GPU work. + // StencilCS already performed the authoritative disocclusion check with the correct + // depth buffer state — no redundant depth agreement check here. + float reprojDepth = centerDepth; + + // First-pass reprojection to find Eye 0 source pixel + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); + if (!r.valid) + return; + + // Save first-pass result as fallback before POM adjustment + Stereo::StereoBilateralResult firstPassR = r; + + // Read POM offset from dedicated POM texture (R16_FLOAT, written by Lighting PS at u7). + // pixelOffset = parallaxAmount (0-1) from ExtendedMaterials, 0.5 = geometry plane. + // Values > 0.5 are peaks (closer to camera), < 0.5 are valleys (farther from camera). + // Correction: high pomOffset should push depth closer (smaller linear depth), + // so we use (0.5 - pomOffset) to get a negative correction for peaks. + // Non-POM pixels store -1.0 (sentinel); hasPOM is encoded by sign: >= 0 means POM ran. + float pomOffset = PomOffsetTexture[r.otherPx]; + if (pomOffset >= 0.0) { + // Re-reproject with POM-adjusted depth centered at geometry plane + float linearDepth = SharedData::GetScreenDepth(centerDepth); + float depthCorrection = (0.5 - pomOffset) * POMDepthScale; + float newLinearDepth = max(linearDepth + depthCorrection, 1e-4); + reprojDepth = (SharedData::CameraData.x - SharedData::CameraData.w / newLinearDepth) / SharedData::CameraData.z; + r = Stereo::ReprojectToOtherEye(uv, reprojDepth, eyeIndex, FrameDim); + if (!r.valid) + r = firstPassR; // Fall back to non-POM reprojection + } + + // Skip if the Eye 0 source pixel is sky/unrendered (depth at clear value). + // At DeferredPasses time, sky hasn't rendered yet — source would have clear color. + // Let the sky/water pass fill these pixels later instead. + float sourceDepth = DepthTexture[r.otherPx]; + if (sourceDepth >= 1.0 || sourceDepth < 1e-5) { + // POM adjustment landed on sky — try the original first-pass source + if (r.otherPx.x != firstPassR.otherPx.x || r.otherPx.y != firstPassR.otherPx.y) { + float fallbackDepth = DepthTexture[firstPassR.otherPx]; + if (fallbackDepth < 1.0 && fallbackDepth >= 1e-5) { + r = firstPassR; + } else { + return; + } + } else { + return; + } + } + + OutputRW[dtid] = SampleReprojectedColor(r.otherStereoUV, FrameDim); + MotionRW[dtid] = MotionRW[r.otherPx]; + +#else // Normal bilateral blend path + +# ifdef EYE0_ONLY + // Only process Eye 0 (left half) - Eye 1 left untouched + float2 uvCheck = (dtid + 0.5) * RcpFrameDim; + if (Stereo::GetEyeIndexFromTexCoord(uvCheck) == 1) + return; +# endif + + float2 uv = (dtid + 0.5) * RcpFrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float4 centerColor = ColorTexture[dtid]; + float centerDepth = DepthTexture[dtid]; + + // Debug states: + // 0 = mask/sky: skipped (depth == 0 or 1) + // 1 = source edge: depth discontinuity at this pixel + // 2 = destination edge: depth discontinuity at reprojected pixel + // 3 = out of bounds: reprojection left the other eye's frame + // 4 = blended, back-check passed: surfaces match in both eyes + // 5 = blended, back-check failed: blend penalized (occlusion edge) + uint debugState = 0; + + Stereo::StereoBilateralResult r = (Stereo::StereoBilateralResult)0; + float4 blendedColor = centerColor; + + // depth == 0.0: VR HMD mask (pixels outside the lens area, never written by the engine) + // depth == 1.0: sky/far plane (no real geometry, bilateral reprojection not meaningful) + bool isSkipPixel = centerDepth < 1e-5 || centerDepth >= 1.0; + if (!isSkipPixel) { + // Source edge detection: skip at depth discontinuities (arm/world silhouettes, + // object edges). Saves VP reprojection work and prevents halo artifacts. + float4 srcEdgeDepths = SampleCrossDepths(dtid, 1, eyeIndex); + if (Stereo::MaxDepthDiff(centerDepth, srcEdgeDepths) > kEdgeDepthThreshold) { + debugState = 1; + } else { + r = Stereo::ReprojectToOtherEye(uv, centerDepth, eyeIndex, FrameDim); + if (r.valid) { + float otherDepth = DepthTexture[r.otherPx]; + + float4 dstEdgeDepths = SampleCrossDepths(r.otherPx, kEdgeMargin, 1 - eyeIndex); + if (any(dstEdgeDepths < 1e-5) || Stereo::MaxDepthDiff(otherDepth, dstEdgeDepths) > kEdgeDepthThreshold) { + debugState = 2; + } else { + float4 otherColor = ColorTexture[r.otherPx]; + Stereo::FinalizeStereoBlend(r, uv, centerDepth, otherDepth, eyeIndex, FrameDim, DepthSigma, MaxBlendFactor); + + float colorDiff = abs(dot(centerColor.rgb, float3(0.2126, 0.7152, 0.0722)) - + dot(otherColor.rgb, float3(0.2126, 0.7152, 0.0722))); + float colorGate = smoothstep(ColorDiffThreshold * 0.5, ColorDiffThreshold * 2.0, colorDiff); + r.blendWeight *= colorGate; + + blendedColor = lerp(centerColor, otherColor, r.blendWeight); + debugState = r.backCheckPassed ? 4 : 5; + } + } else { + debugState = 3; + } + } + } + +# ifdef DEBUG_BACKCHECK + // Debug visualization (6 states): + // Blue = mask/sky: skipped + // Yellow = source edge: depth discontinuity at this pixel + // Orange = destination edge: depth discontinuity at reprojected pixel + // Grey = out of bounds: other eye can't see this point + // Green = back-check passed: surfaces match in both eyes + // Red = back-check failed: blend penalized (occlusion edge) + float3 debugColors[6] = { + float3(0.1, 0.1, 0.5), // 0: mask/sky - blue + float3(0.8, 0.8, 0.0), // 1: source edge - yellow + float3(0.8, 0.4, 0.0), // 2: destination edge - orange + float3(0.3, 0.3, 0.3), // 3: out of bounds - grey + float3(0.0, 0.5, 0.0), // 4: back-check passed - green + float3(0.5, 0.0, 0.0) // 5: back-check failed - red + }; + OutputRW[dtid] = float4(lerp(centerColor.rgb, debugColors[debugState], 0.7), centerColor.a); +# elif defined(DEBUG_BLEND_WEIGHT) + // Blend weight heatmap: only pixels with actual blend activity are colorized. + // Untouched pixels pass through unmodified. + float w = saturate(r.blendWeight / max(MaxBlendFactor, 1e-5)); + if (w > 1e-3) { + float3 heatmap = Color::TurboColormap(w); + OutputRW[dtid] = float4(lerp(centerColor.rgb, saturate(heatmap), 0.8), centerColor.a); + } else { + OutputRW[dtid] = centerColor; + } +# elif defined(DEBUG_EDGE_DETECTION) + // Edge detection visualizer: highlights pixels excluded by depth discontinuity checks. + // Non-edge pixels show the normal blended output for scene context. + // Bright yellow = source edge: discontinuity at this pixel + // Bright orange = destination edge: discontinuity at reprojected pixel + if (debugState == 1) { + OutputRW[dtid] = float4(lerp(centerColor.rgb, float3(1.0, 1.0, 0.0), 0.8), centerColor.a); + } else if (debugState == 2) { + OutputRW[dtid] = float4(lerp(centerColor.rgb, float3(1.0, 0.5, 0.0), 0.8), centerColor.a); + } else { + OutputRW[dtid] = blendedColor; + } +# else + OutputRW[dtid] = blendedColor; +# endif + +#endif // STEREO_OVERWRITE +} diff --git a/package/Shaders/VR/VRPostProcessCS.hlsl b/package/Shaders/VR/VRPostProcessCS.hlsl new file mode 100644 index 0000000000..770e244553 --- /dev/null +++ b/package/Shaders/VR/VRPostProcessCS.hlsl @@ -0,0 +1,109 @@ +// VR Post-Process - Bilateral blend for near-camera 2x supersampling +// +// Runs after all compositing and stereo blending is complete. +// Reads per-pixel classification from StencilCS and applies: +// - MODE_FULL_BLEND: bilateral depth-weighted blend for 2x supersampling +// +// Only MODE_FULL_BLEND pixels are processed. All others pass through untouched. + +#include "Common/FrameBuffer.hlsli" +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" + +Texture2D ColorTexture : register(t0); // Copy of final composited image +Texture2D ModeTexture : register(t1); +Texture2D DepthTexture : register(t2); + +RWTexture2D OutputRW : register(u0); + +cbuffer VRPostProcessCB : register(b1) +{ + float2 FrameDim; + float2 RcpFrameDim; + float DebugEdgeTint; // 0 = off, >0 = debug visualization strength + uint DebugMode; // 0 = normal, 1 = depth map diagnostic, 2 = full blend depth visualizer + float FullBlendDistance; // Linearized depth threshold for full blend zone visualization + float _pad; // Pad to 16-byte alignment +}; + +#include "VRStereoOptimizations/modes.hlsli" + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + + uint pixelMode = ModeTexture[dtid]; + + // Depth map diagnostic: show mode texture contents as solid colors + if (DebugMode == 1) { + float4 c = ColorTexture[dtid]; + if (pixelMode == MODE_EDGE) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 0), 0.5), c.a); + else if (pixelMode == MODE_EDGE_NEIGHBOUR) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0, 1), 0.5), c.a); + else if (pixelMode == MODE_DISOCCLUDED) + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 0.5, 1), 0.3), c.a); + else if (pixelMode == MODE_FULL_BLEND) + OutputRW[dtid] = float4(lerp(c.rgb, float3(1, 0.5, 0), 0.5), c.a); // Orange = full blend zone + return; + } + + // Full blend depth visualizer: shows the depth boundary as a cyan tint + if (DebugMode == 2) { + float2 uvDb = (dtid + 0.5) * RcpFrameDim; + float depthDb = DepthTexture[dtid]; + if (depthDb < 1e-5 || depthDb >= 1.0) + return; + float linDepth = SharedData::GetScreenDepth(depthDb); + if (linDepth < FullBlendDistance) { + float4 c = ColorTexture[dtid]; + float proximity = saturate(1.0 - linDepth / max(FullBlendDistance, 1.0)); + OutputRW[dtid] = float4(lerp(c.rgb, float3(0, 1, 1), proximity * 0.4), c.a); + } + return; + } + + // Only process full blend pixels + if (pixelMode != MODE_FULL_BLEND) + return; + + float2 uv = (dtid + 0.5) * RcpFrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + float4 result = ColorTexture[dtid]; + + // === MODE_FULL_BLEND: bilateral blend for 2x supersampling === + { + float4 center = result; + float centerDepth = DepthTexture[dtid]; + + // Reproject to the other eye + Stereo::StereoBilateralResult r = Stereo::ReprojectToOtherEye(uv, centerDepth, eyeIndex, FrameDim); + if (!r.valid) { + // Debug tint for failed reprojection + if (DebugEdgeTint > 0) + OutputRW[dtid] = float4(lerp(center.rgb, float3(1, 0.5, 0), DebugEdgeTint), center.a); + return; + } + + // Only blend with pixels that have valid composited data in both eyes. + uint otherMode = ModeTexture[r.otherPx]; + if (otherMode != MODE_FULL_BLEND && otherMode != MODE_DISOCCLUDED) + return; + + float4 otherColor = ColorTexture[r.otherPx]; + float otherDepth = DepthTexture[r.otherPx]; + + // Depth-weighted bilateral blend + float maxDepth = max(max(centerDepth, otherDepth), 1e-5); + float depthAgreement = 1.0 - saturate(abs(centerDepth - otherDepth) / maxDepth / 0.02); + float blendWeight = 0.5 * depthAgreement; + + result = lerp(center, otherColor, blendWeight); + + if (DebugEdgeTint > 0) + result.rgb = lerp(result.rgb, float3(0, 1, 1), DebugEdgeTint); + } + + OutputRW[dtid] = result; +} diff --git a/package/Shaders/VRStereoOptimizations/StencilCS.hlsl b/package/Shaders/VRStereoOptimizations/StencilCS.hlsl new file mode 100644 index 0000000000..8a66a7e676 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilCS.hlsl @@ -0,0 +1,172 @@ +// VR Stereo Optimizations - Stencil Classification Compute Shader +// +// Classifies BOTH eyes over the full SBS buffer. Each pixel is tagged as: +// MODE_DISOCCLUDED - Must be fully shaded (sky, HMD mask, parallax-occluded) +// MODE_EDGE - Depth edge boundary (dist 1) or inner/foreground band; fully shaded + bilateral blend +// MODE_MAIN - Standard pixel eligible for reprojection / bilateral blend +// MODE_FULL_BLEND - Near-camera geometry: both eyes fully shaded for 2x supersampling +// +// Dispatched over full SBS resolution (FrameDim.x x FrameDim.y). + +#include "Common/SharedData.hlsli" +#include "Common/VR.hlsli" +#include "VRStereoOptimizations/cbuffers.hlsli" + +Texture2D DepthTexture : register(t0); + +RWTexture2D ModeTextureRW : register(u0); + +// Sentinel for the edge-detection search: means "no discontinuity found yet". +static const uint kEdgeDistNone = 0xFFFFFFFFu; + +[numthreads(8, 8, 1)] void main(uint2 dtid : SV_DispatchThreadID) { + if (any(dtid >= uint2(FrameDim))) + return; + + // Determine which eye this pixel belongs to + float2 uv = (float2(dtid) + 0.5) / FrameDim; + uint eyeIndex = Stereo::GetEyeIndexFromTexCoord(uv); + + // Read depth directly in SBS coords + float centerDepth = DepthTexture[dtid]; + +#ifdef DEBUG_DEPTH_MAP + // DIAGNOSTIC: Visualize what depth values StencilCS sees. + // Green (MODE_EDGE) = depth >= 1.0 (HMD mask threshold) + // Magenta (MODE_EDGE_NEIGHBOUR) = depth < EPSILON_DEPTH_SKY (sky threshold) + // No tint (MODE_MAIN) = normal geometry with valid depth + if (centerDepth >= 1.0) { + ModeTextureRW[dtid] = MODE_EDGE; + return; + } + if (centerDepth < EPSILON_DEPTH_SKY) { + ModeTextureRW[dtid] = MODE_EDGE_NEIGHBOUR; + return; + } + ModeTextureRW[dtid] = MODE_MAIN; + return; +#endif + + // Sky/unrendered pixels (depth >= 1.0 at z-prepass time = depth buffer clear value) + // and HMD mask pixels both have depth >= 1.0 here. Treat them the same as sky: + // let edge detection run so geometry-vs-sky boundaries get classified. + // HMD mask pixels are in lens corners with no nearby geometry, so they'll + // fall through to MODE_DISOCCLUDED at the end. + bool isSky = (centerDepth < EPSILON_DEPTH_SKY) || (centerDepth >= 1.0); + float linCenter = isSky ? DEPTH_SKY_SENTINEL : SharedData::GetScreenDepth(centerDepth); + + // Near-camera supersampling: geometry closer than FullBlendDistance gets full + // shading in both eyes for bilateral blend (2x supersampling in VRPostProcess). + if (!isSky && linCenter < FullBlendDistance) { + ModeTextureRW[dtid] = MODE_FULL_BLEND; + return; + } + + // --- Disocclusion detection via reprojection (runs for all non-sky pixels) --- + // Early return: disoccluded pixels are always MODE_DISOCCLUDED regardless of edge proximity. + // This ensures MinEdgeDistance never affects disocclusion classification. + if (!isSky) { + Stereo::StereoBilateralResult reproj = Stereo::ReprojectToOtherEye( + uv, + centerDepth, + eyeIndex, + FrameDim); + + bool isDisoccluded = false; + if (!reproj.valid) { + isDisoccluded = true; + } else { + float otherDepth = DepthTexture[reproj.otherPx]; + // Raw reversed-Z depth comparison for disocclusion detection. + // Using raw depth avoids concentric semicircle artifacts that occur + // with linearized depth due to precision band boundaries in the + // hyperbolic depth-to-linear conversion. + float maxRaw = max(max(centerDepth, otherDepth), EPSILON_DIVISION); + float rawRelDiff = abs(centerDepth - otherDepth) / maxRaw; + isDisoccluded = (rawRelDiff > DisocclusionThreshold); + + // Directional disocclusion: catches silhouette edge pixels where both eyes sample + // similar linearized depth but Eye 0's color is wrong for Eye 1. These slip through + // the symmetric rawRelDiff check above. The condition fires when Eye 0 is at similar + // or slightly closer depth than Eye 1 (scale < 1.0), marking them disoccluded so Eye 1 + // renders natively. ForwardOcclusionScale=0.5 triggers when Eye 0 is less than 2x Eye 1's + // linearized depth; lower values are more aggressive, 0 = disabled. + if (!isDisoccluded && eyeIndex == 1 && ForwardOcclusionScale > 0.0) { + bool otherIsSky = (otherDepth < EPSILON_DEPTH_SKY) || (otherDepth >= 1.0); + if (!otherIsSky) { + float linOther = SharedData::GetScreenDepth(otherDepth); + isDisoccluded = (linOther * ForwardOcclusionScale < linCenter); + } + } + } + + if (isDisoccluded) { + ModeTextureRW[dtid] = MODE_DISOCCLUDED; + return; + } + } + + // Depth gate: skip edge detection for nearby geometry (saves perf, distant AA matters more) + // Sky pixels always run edge detection — they need to expand the edge band outward. + // Disocclusion detection (above) is independent of this gate and always runs. + bool skipEdgeDetection = !isSky && (linCenter < MinEdgeDistance); + + // --- Edge detection with two-tier classification --- + // MODE_EDGE: immediate neighbor (distance 1) has depth discontinuity, OR + // inner/foreground band (distance <= kInnerWidth). + // kInnerWidth=4 provides enough margin at high VR resolutions (~8k wide) to catch + // disocclusion boundary pixels that are just outside the immediate-neighbor band. + static const uint kInnerWidth = 4; + int2 offsets[4] = { int2(-1, 0), int2(1, 0), int2(0, -1), int2(0, 1) }; + + uint nearestEdgeDist = kEdgeDistNone; // nearest distance at which a discontinuity was found + bool nearestWeAreOuter = false; // whether we are on the background side at that nearest hit + + // Use the larger of inner/outer widths for the search + uint maxWidth = kInnerWidth; + + if (!skipEdgeDetection) { + [loop] for (uint d = 1; d <= maxWidth; d++) + { + [unroll] for (int i = 0; i < 4; i++) + { + int2 rawNeighbor = int2(dtid) + offsets[i] * (int)d; + uint2 neighborCoord = Stereo::ClampToEyeBounds(rawNeighbor, eyeIndex, FrameDim); + + float neighborDepth = DepthTexture[neighborCoord]; + bool neighborIsSky = (neighborDepth < EPSILON_DEPTH_SKY) || (neighborDepth >= 1.0); + float linNeighbor = neighborIsSky ? DEPTH_SKY_SENTINEL : SharedData::GetScreenDepth(neighborDepth); + float maxLin = max(max(linCenter, linNeighbor), EPSILON_DEPTH_SKY); + float relDepthDiff = abs(linCenter - linNeighbor) / maxLin; + + if (relDepthDiff > EdgeDepthThreshold && d < nearestEdgeDist) { + nearestEdgeDist = d; + nearestWeAreOuter = (linNeighbor < linCenter); // neighbor closer to camera = we are background + } + } + } + + } // !skipEdgeDetection + + if (nearestEdgeDist != kEdgeDistNone) { + // Classify based on distance and side + if (nearestEdgeDist == 1) { + // Immediate neighbor discontinuity: always MODE_EDGE regardless of side + ModeTextureRW[dtid] = MODE_EDGE; + return; + } else if (!nearestWeAreOuter && nearestEdgeDist <= kInnerWidth) { + // Inner/foreground band beyond distance 1 + ModeTextureRW[dtid] = MODE_EDGE; + return; + } + } + + // Sky pixels that aren't near edges -> disoccluded (reprojection is meaningless for sky) + if (isSky) { + ModeTextureRW[dtid] = MODE_DISOCCLUDED; + return; + } + + // Standard pixel + ModeTextureRW[dtid] = MODE_MAIN; +} diff --git a/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl new file mode 100644 index 0000000000..6e49007035 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilWritePS.hlsl @@ -0,0 +1,40 @@ +// VR Stereo Optimizations - Stencil Write Pixel Shader +// +// Reads from the per-pixel mode classification texture. +// Only MODE_MAIN pixels write stencil ref=1 — these are reprojected by ReprojectionCS +// and must be skipped by the geometry pass (NOT_EQUAL stencil test, ref=1). +// +// All other modes (DISOCCLUDED, EDGE, EDGE_NEIGHBOUR, FULL_BLEND) discard so +// geometry renders those pixels normally. ReprojectionCS only fills MODE_MAIN, so +// stencil must not be written for any other mode. +// +// Mode texture is full SBS resolution (same as render target). +// The DSS is configured with StencilFunc=ALWAYS, StencilPassOp=REPLACE, ref=1. +// Pixels that survive (not discarded) get stencil=1 written. + +#include "VRStereoOptimizations/cbuffers.hlsli" + +Texture2D ModeTexture : register(t0); + +struct PS_INPUT +{ + float4 Position: SV_Position; + float2 TexCoord: TEXCOORD0; +}; + +void main(PS_INPUT input) +{ + // Mode texture is full SBS resolution — SV_Position maps directly + // (viewport is Eye 1 half, so SV_Position.x starts at eyeWidth) + int2 modeCoord = int2(input.Position.xy); + + uint mode = ModeTexture[modeCoord]; + + // Only MODE_MAIN pixels are filled by ReprojectionCS and should be stencil-culled. + // EDGE/EDGE_NEIGHBOUR/FULL_BLEND must render normally; DISOCCLUDED is also fully shaded. + if (mode != MODE_MAIN) + discard; + + // Pixel survives: DSS writes stencil ref=1 + // No color output (no RTV bound) +} diff --git a/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl b/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl new file mode 100644 index 0000000000..353aa53379 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/StencilWriteVS.hlsl @@ -0,0 +1,24 @@ +// VR Stereo Optimizations - Stencil Write Vertex Shader +// +// Procedural fullscreen triangle covering Eye 1 (right half of SBS buffer). +// No vertex buffer needed — vertex positions are generated from SV_VertexID. +// The viewport is set to Eye 1 by the C++ code, so we just emit a standard +// fullscreen triangle in clip space. + +struct VS_OUTPUT +{ + float4 Position: SV_Position; + float2 TexCoord: TEXCOORD0; +}; + +VS_OUTPUT main(uint vertexID : SV_VertexID) +{ + VS_OUTPUT output; + + // Fullscreen triangle: 3 vertices covering [-1,1] clip space + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + output.Position = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); + output.TexCoord = uv; + + return output; +} diff --git a/package/Shaders/VRStereoOptimizations/cbuffers.hlsli b/package/Shaders/VRStereoOptimizations/cbuffers.hlsli new file mode 100644 index 0000000000..a7fb7a3961 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/cbuffers.hlsli @@ -0,0 +1,31 @@ +// VR Stereo Optimizations - Shared constant buffer layout +// Must match VRStereoOptParams in VRStereoOptimizations.h exactly + +#ifndef __VR_STEREO_OPT_CBUFFERS_HLSLI__ +#define __VR_STEREO_OPT_CBUFFERS_HLSLI__ + +cbuffer VRStereoOptParams : register(b1) +{ + float2 FrameDim; // Full stereo buffer dimensions (both eyes) + float2 RcpFrameDim; // 1.0 / FrameDim + + uint StereoModeValue; // 0=Off, 1=Enable + float DisocclusionThreshold; // Depth difference threshold for disocclusion detection + float EdgeDepthThreshold; // Relative depth difference threshold for edge detection + uint EdgeWidth; // Half-width of edge detection band in pixels + + float2 QualityJitter; // Sub-pixel jitter offset (Quality mode) + float FoveatedRadius; // Radius of foveal region in UV space + float ForwardOcclusionScale; // Eye 0 depth multiplier for directional disocclusion (0 = disabled) + + float2 FoveatedCenter; // Center of foveal region in UV space + float MinEdgeDistance; + float FullBlendDistance; // Linearized depth below which pixels get MODE_FULL_BLEND (game units) +}; + +#define STEREO_MODE_OFF 0 +#define STEREO_MODE_ENABLE 1 + +#include "VRStereoOptimizations/modes.hlsli" + +#endif diff --git a/package/Shaders/VRStereoOptimizations/modes.hlsli b/package/Shaders/VRStereoOptimizations/modes.hlsli new file mode 100644 index 0000000000..b693dedcc3 --- /dev/null +++ b/package/Shaders/VRStereoOptimizations/modes.hlsli @@ -0,0 +1,10 @@ +#ifndef __VR_STEREO_OPT_MODES_HLSLI__ +#define __VR_STEREO_OPT_MODES_HLSLI__ + +#define MODE_DISOCCLUDED 0 +#define MODE_EDGE 1 +#define MODE_MAIN 2 +#define MODE_EDGE_NEIGHBOUR 3 +#define MODE_FULL_BLEND 4 + +#endif diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index 899ebe588a..54998a24e3 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -71,6 +71,9 @@ struct VS_INPUT float4 Color: COLOR0; # endif # endif +# if defined(VR) + uint InstanceID: SV_INSTANCEID; +# endif // VR }; struct VS_OUTPUT @@ -117,13 +120,21 @@ struct VS_OUTPUT # endif float4 NormalsScale: TEXCOORD8; +# if defined(VR) + float ClipDistance: SV_ClipDistance0; // o11 + float CullDistance: SV_CullDistance0; // p11 +# endif // VR }; # ifdef VSHADER cbuffer PerTechnique : register(b0) { - float4 QPosAdjust : packoffset(c0); +# if !defined(VR) + float4 QPosAdjust[1] : packoffset(c0); +# else + float4 QPosAdjust[2] : packoffset(c0); +# endif // VR }; cbuffer PerMaterial : register(b1) @@ -138,22 +149,35 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { - row_major float4x4 World : packoffset(c0); - row_major float4x4 PreviousWorld : packoffset(c4); - row_major float4x4 WorldViewProj : packoffset(c8); +# if !defined(VR) + row_major float4x4 World[1] : packoffset(c0); + row_major float4x4 PreviousWorld[1] : packoffset(c4); + row_major float4x4 WorldViewProj[1] : packoffset(c8); float3 ObjectUV : packoffset(c12); float4 CellTexCoordOffset : packoffset(c13); +# else // VR has 25 vs 13 entries + row_major float4x4 World[2] : packoffset(c0); + row_major float4x4 PreviousWorld[2] : packoffset(c8); + row_major float4x4 WorldViewProj[2] : packoffset(c16); + float3 ObjectUV : packoffset(c24); + float4 CellTexCoordOffset : packoffset(c25); +# endif // VR }; VS_OUTPUT main(VS_INPUT input) { VS_OUTPUT vsout = (VS_OUTPUT)0; + uint eyeIndex = Stereo::GetEyeIndexVS( +# if defined(VR) + input.InstanceID +# endif + ); vsout.NormalsScale = NormalsScale; float4 inputPosition = float4(input.Position.xyz, 1.0); - float4 worldPos = mul(World, inputPosition); - float4 worldViewPos = mul(WorldViewProj, inputPosition); + float4 worldPos = mul(World[eyeIndex], inputPosition); + float4 worldViewPos = mul(WorldViewProj[eyeIndex], inputPosition); float heightMult = min((1.0 / 10000.0) * max(worldViewPos.z - 70000, 0), 1); @@ -163,7 +187,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(STENCIL) vsout.WorldPosition = worldPos; - vsout.PreviousWorldPosition = mul(PreviousWorld, inputPosition); + vsout.PreviousWorldPosition = mul(PreviousWorld[eyeIndex], inputPosition); # else # if !defined(UNIFIED_WATER) @@ -177,7 +201,7 @@ VS_OUTPUT main(VS_INPUT input) # if defined(LOD) float4 posAdjust = - ObjectUV.x ? 0.0 : (QPosAdjust.xyxy + worldPos.xyxy) / NormalsScale.xxyy; + ObjectUV.x ? 0.0 : (QPosAdjust[eyeIndex].xyxy + worldPos.xyxy) / NormalsScale.xxyy; vsout.TexCoord1.xyzw = NormalsScroll0 + posAdjust; # else @@ -185,7 +209,7 @@ VS_OUTPUT main(VS_INPUT input) vsout.MPosition.xyzw = inputPosition.xyzw; # endif - float2 posAdjust = worldPos.xy + QPosAdjust.xy; + float2 posAdjust = worldPos.xy + QPosAdjust[eyeIndex].xy; float2 scrollAdjust1 = posAdjust / NormalsScale.xx; float2 scrollAdjust2 = posAdjust / NormalsScale.yy; @@ -272,6 +296,12 @@ VS_OUTPUT main(VS_INPUT input) # endif # endif +# ifdef VR + Stereo::VR_OUTPUT VRout = Stereo::GetVRVSOutput(vsout.HPosition, eyeIndex); + vsout.HPosition = VRout.VRPosition; + vsout.ClipDistance.x = VRout.ClipDistance; + vsout.CullDistance.x = VRout.CullDistance; +# endif // VR return vsout; } @@ -321,11 +351,19 @@ Texture2D RawSSRReflectionTex : register(t11); cbuffer PerTechnique : register(b0) { +# if !defined(VR) float4 VPOSOffset : packoffset(c0); // inverse main render target width and height in xy, 0 in zw - float4 PosAdjust : packoffset(c1); // inverse framebuffer range in w + float4 PosAdjust[1] : packoffset(c1); // inverse framebuffer range in w float4 CameraDataWater : packoffset(c2); float4 SunDir : packoffset(c3); float4 SunColor : packoffset(c4); +# else + float4 VPOSOffset : packoffset(c0); // inverse main render target width and height in xy, 0 in zw + float4 PosAdjust[2] : packoffset(c1); // inverse framebuffer range in w + float4 CameraDataWater : packoffset(c3); + float4 SunDir : packoffset(c4); + float4 SunColor : packoffset(c5); +# endif } cbuffer PerMaterial : register(b1) @@ -348,13 +386,43 @@ cbuffer PerMaterial : register(b1) cbuffer PerGeometry : register(b2) { - float4x4 TextureProj : packoffset(c0); +# if !defined(VR) + float4x4 TextureProj[1] : packoffset(c0); float4 ReflectPlane[1] : packoffset(c4); float4 ProjData : packoffset(c5); float4 LightPos[8] : packoffset(c6); float4 LightColor[8] : packoffset(c14); +# else + float4x4 TextureProj[2] : packoffset(c0); + float4 ReflectPlane[2] : packoffset(c8); + float4 ProjData : packoffset(c10); + float4 LightPos[8] : packoffset(c11); + float4 LightColor[8] : packoffset(c19); +# endif //VR } +# if defined(VR) +/** +Calculates the depthMultiplier as used in Water.hlsl + +VR appears to require use of CameraProjInverse and does not use ProjData +@param uv UV coords to convert +@param depth The calculated depth +@param eyeIndex The eyeIndex; 0 is left, 1 is right +@returns depthMultiplier +*/ +float CalculateDepthMultFromUV(float2 uv, float depth, uint eyeIndex = 0) +{ + float4 temp; + temp.xy = (uv * 2 - 1); + temp.z = depth; + temp.w = 1; + temp = mul(FrameBuffer::CameraProjInverse[eyeIndex], temp.xyzw); + temp.xyz /= temp.w; + return length(temp.xyz); +} +# endif // VR + # define SampColorSampler Normals01Sampler # define LinearSampler Normals01Sampler @@ -369,14 +437,14 @@ cbuffer PerGeometry : register(b2) # include "Common/ShadowSampling.hlsli" # if defined(SIMPLE) || defined(UNDERWATER) || defined(LOD) || defined(SPECULAR) -float GetWaterFogFade() +float GetWaterFogFade(uint eyeIndex) { # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - return ExponentialHeightFog::GetVanillaFogFade(PosAdjust.w); + return ExponentialHeightFog::GetVanillaFogFade(PosAdjust[eyeIndex].w); } # endif - return PosAdjust.w; + return PosAdjust[eyeIndex].w; } # if defined(FLOWMAP) @@ -507,7 +575,11 @@ float GetFlowmapMipLevel(float2 flowmapUV) float2 textureDims; FlowMapNormalsTex.GetDimensions(textureDims.x, textureDims.y); +# if defined(VR) + textureDims /= 16.0; +# else textureDims /= 8.0; +# endif float2 texCoordsPerSize = flowmapUV * textureDims; float2 dxSize = ddx(texCoordsPerSize); @@ -609,7 +681,7 @@ struct WaterNormalData float4 rippleInfo; // xyz = scaled ripple normal (normalized normal * intensity), w = splash effect intensity }; -WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, float wetnessOcclusion) +WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, uint eyeIndex, float wetnessOcclusion) { WaterNormalData result; result.rippleInfo = float4(0, 0, 0, 0); @@ -756,7 +828,7 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma rippleWPosition.xy += flowOffset; # endif - raindropInfo = WetnessEffects::GetRainDrops(rippleWPosition + FrameBuffer::CameraPosAdjust.xyz, SharedData::wetnessEffectsSettings.Time, finalNormal, rippleStrengthModifier); + raindropInfo = WetnessEffects::GetRainDrops(rippleWPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, SharedData::wetnessEffectsSettings.Time, finalNormal, rippleStrengthModifier); // Calculate ripple and splash color intensities float rippleIntensity = length(raindropInfo.xy) * rippleStrengthModifier; @@ -800,8 +872,14 @@ float3 GetWaterSpecularColor(PS_INPUT input, float3 normal, float3 viewDirection float reflectionAmount = saturate(length(input.WPosition.xyz) / 1024.0); +# if defined(VR) + // Reflection cubemap is incorrect for interiors in VR, ignore it + if (Permutation::PixelShaderDescriptor & Permutation::WaterFlags::Interior || SharedData::HideSky) + reflectionAmount = 0.0; +# else if (SharedData::HideSky) reflectionAmount = 0.0; +# endif reflectionColor = lerp(dynamicCubemap, reflectionColor, reflectionAmount); # endif @@ -824,10 +902,14 @@ float3 GetWaterSpecularColor(PS_INPUT input, float3 normal, float3 viewDirection return reflectionColor; } -float GetScreenDepthWater(float2 screenPosition) +float GetScreenDepthWater(float2 screenPosition, uint a_useVR = 0) { float depth = DepthTex.Load(float3(screenPosition, 0)).x; +# if defined(VR) // VR appears to use hard coded values + return depth * 1.01 + -0.01; +# else return (CameraDataWater.w / (-depth * CameraDataWater.z + CameraDataWater.x)); +# endif } float3 GetLdotN(float3 normal) @@ -861,12 +943,18 @@ struct DiffuseOutput float3 refractedViewDirection; }; -DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection, inout float4 distanceMul, float refractionsDepthFactor, float fresnel, float3 viewPosition, float depth) +DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDirection, inout float4 distanceMul, float refractionsDepthFactor, float fresnel, uint eyeIndex, float3 viewPosition, float depth) { # if defined(REFRACTIONS) - float4 refractionNormal = mul(transpose(TextureProj), float4((VarAmounts.w * refractionsDepthFactor * normal.xy) + input.MPosition.xy, input.MPosition.z, 1)); + float4 refractionNormal = mul(transpose(TextureProj[eyeIndex]), float4((VarAmounts.w * refractionsDepthFactor * normal.xy) + input.MPosition.xy, input.MPosition.z, 1)); float2 refractionUvRaw = float2(refractionNormal.x, refractionNormal.w - refractionNormal.y) / refractionNormal.ww; + refractionUvRaw = Stereo::ConvertToStereoUV(refractionUvRaw, eyeIndex); // need to convert here for VR due to refractionNormal values + +# if defined(VR) + float2 refractionUvRawNoStereo = Stereo::ConvertFromStereoUV(refractionUvRaw, eyeIndex, 1); +# endif + float2 screenPosition = FrameBuffer::DynamicResolutionParams1.xy * (FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy); float2 refractionScreenPosition = FrameBuffer::DynamicResolutionParams1.xy * (refractionUvRaw / VPOSOffset.xy); @@ -875,19 +963,27 @@ DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDir # if defined(DEPTH) && !defined(VERTEX_ALPHA_DEPTH) float refractionDepth = GetScreenDepthWater(refractionScreenPosition); depth = refractionDepth; +# if !defined(VR) float refractionDepthMul = length(float3((((VPOSOffset.zw + refractionUvRaw) * 2 - 1)) * refractionDepth / ProjData.xy, refractionDepth)); +# else + float refractionDepthMul = CalculateDepthMultFromUV(refractionUvRawNoStereo, refractionDepth, eyeIndex); +# endif //VR float3 refractionDepthAdjustedViewDirection = -viewDirection * refractionDepthMul; - float refractionViewSurfaceAngle = dot(refractionDepthAdjustedViewDirection, ReflectPlane[0].xyz); + float refractionViewSurfaceAngle = dot(refractionDepthAdjustedViewDirection, ReflectPlane[eyeIndex].xyz); - float refractionPlaneMul = (1 - ReflectPlane[0].w / refractionViewSurfaceAngle); + float refractionPlaneMul = (1 - ReflectPlane[eyeIndex].w / refractionViewSurfaceAngle); if (refractionPlaneMul < 0.0) { - refractionUvRaw = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; + refractionUvRaw = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; // This value is already stereo converted for VR } else { distanceMul = saturate(refractionPlaneMul * float4(length(refractionDepthAdjustedViewDirection).xx, abs(refractionViewSurfaceAngle).xx) / FogParam.z); - refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse, float4((refractionUvRaw * 2 - 1) * float2(1, -1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); +# if defined(VR) + refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4((refractionUvRawNoStereo * 2 - 1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); +# else + refractionWorldPosition = mul(FrameBuffer::CameraViewProjInverse[eyeIndex], float4((refractionUvRaw * 2 - 1) * float2(1, -1), DepthTex.Load(float3(refractionScreenPosition, 0)).x, 1)); +# endif refractionWorldPosition.xyz /= refractionWorldPosition.w; } # endif @@ -920,7 +1016,7 @@ DiffuseOutput GetWaterDiffuseColor(PS_INPUT input, float3 normal, float3 viewDir # endif } -float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition) +float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition, uint eyeIndex) { # if defined(UNDERWATER) return 0.0.xxx; @@ -935,7 +1031,7 @@ float3 GetSunColor(float3 normal, float3 viewDirection, float3 worldPosition) float3 sunColor = Color::DirectionalLight((SunColor.xyz * SunDir.w) / max(llDirLightMult, 1e-5), SharedData::linearLightingSettings.isDirLightLinear) * (1.0 - exp(-DeepColor.w)) * llDirLightMult; # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - sunColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust.xyz); + sunColor *= ExponentialHeightFog::GetSunlightFogAttenuation(worldPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz); } # endif return reflectionMul * sunColor; @@ -959,6 +1055,7 @@ PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT psout; + uint eyeIndex = Stereo::GetEyeIndexPS(input.HPosition, VPOSOffset); float2 screenPosition = FrameBuffer::DynamicResolutionParams1.xy * (FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy); # if defined(SIMPLE) || defined(UNDERWATER) || defined(LOD) || defined(SPECULAR) @@ -986,11 +1083,15 @@ PS_OUTPUT main(PS_INPUT input) depth = GetScreenDepthWater(screenPosition); float2 depthOffset = FrameBuffer::DynamicResolutionParams2.xy * input.HPosition.xy * VPOSOffset.xy + VPOSOffset.zw; +# if !defined(VR) float depthMul = length(float3((depthOffset * 2 - 1) * depth / ProjData.xy, depth)); +# else + float depthMul = CalculateDepthMultFromUV(Stereo::ConvertFromStereoUV(depthOffset, eyeIndex, 1), depth, eyeIndex); +# endif //VR float3 depthAdjustedViewDirection = -viewDirection * depthMul; - float viewSurfaceAngle = dot(depthAdjustedViewDirection, ReflectPlane[0].xyz); + float viewSurfaceAngle = dot(depthAdjustedViewDirection, ReflectPlane[eyeIndex].xyz); - float planeMul = (1 - ReflectPlane[0].w / viewSurfaceAngle); + float planeMul = (1 - ReflectPlane[eyeIndex].w / viewSurfaceAngle); distanceMul = saturate( planeMul * float4(length(depthAdjustedViewDirection).xx, abs(viewSurfaceAngle).xx) / FogParam.z); @@ -1006,14 +1107,18 @@ PS_OUTPUT main(PS_INPUT input) # else float4 depthControl = DepthControl * (distanceMul - 1) + 1; # endif - float3 viewPosition = mul(FrameBuffer::CameraView, float4(input.WPosition.xyz, 1)).xyz; - float2 screenUV = FrameBuffer::ViewToUV(viewPosition); + float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WPosition.xyz, 1)).xyz; + float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); # if defined(SKYLIGHTING) float wetnessOcclusion = 1.0; +# if defined(VR) + float3 positionMSSkylight = input.WPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else float3 positionMSSkylight = input.WPosition.xyz; +# endif sh2 skylightingSH = Skylighting::SampleNoBias(positionMSSkylight); float skylighting = SphericalHarmonics::Unproject(skylightingSH, float3(0, 0, 1)); @@ -1024,9 +1129,9 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(SKYLIGHTING) - WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, wetnessOcclusion); + WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, eyeIndex, wetnessOcclusion); # else - WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, inWorld); + WaterNormalData waterData = GetWaterNormal(input, distanceBlendFactor, depthControl.z, viewDirection, depth, eyeIndex, inWorld); # endif float3 normal = waterData.normal; @@ -1043,7 +1148,7 @@ PS_OUTPUT main(PS_INPUT input) [unroll] for (int lightIndex = 0; lightIndex < NUM_SPECULAR_LIGHTS; ++lightIndex) { - float3 lightVector = LightPos[lightIndex].xyz - (PosAdjust.xyz + input.WPosition.xyz); + float3 lightVector = LightPos[lightIndex].xyz - (PosAdjust[eyeIndex].xyz + input.WPosition.xyz); float3 lightDirection = normalize(normalize(lightVector) - viewDirection); float lightFade = saturate(length(lightVector) / LightPos[lightIndex].w); float lightColorMul = (1 - lightFade * lightFade); @@ -1070,10 +1175,10 @@ PS_OUTPUT main(PS_INPUT input) float3 specularColor = GetWaterSpecularColor(input, normal, viewDirection, distanceFactor, 1.0); # endif - DiffuseOutput diffuseOutput = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, viewPosition, depth); + DiffuseOutput diffuseOutput = GetWaterDiffuseColor(input, normal, viewDirection, distanceMul, depthControl.y, fresnel, eyeIndex, viewPosition, depth); float surfaceShadow; - float dirShadow = ShadowSampling::Get3DFilteredShadow(input.WPosition.xyz, diffuseOutput.refractedViewDirection, input.HPosition.xy, surfaceShadow); + float dirShadow = ShadowSampling::Get3DFilteredShadow(input.WPosition.xyz, diffuseOutput.refractedViewDirection, input.HPosition.xy, eyeIndex, surfaceShadow); float3 dirColor; float3 ambientColor; @@ -1114,7 +1219,7 @@ PS_OUTPUT main(PS_INPUT input) continue; } - float3 lightDirection = light.positionWS.xyz - input.WPosition.xyz; + float3 lightDirection = light.positionWS[eyeIndex].xyz - input.WPosition.xyz; float lightDist = length(lightDirection); # if defined(ISL) @@ -1150,7 +1255,7 @@ PS_OUTPUT main(PS_INPUT input) # endif # else - float3 sunColor = GetSunColor(normal, viewDirection, input.WPosition.xyz) * surfaceShadow; + float3 sunColor = GetSunColor(normal, viewDirection, input.WPosition.xyz, eyeIndex) * surfaceShadow; # if defined(VC) float specularFraction = lerp(1, fresnel * diffuseOutput.refractionMul, distanceBlendFactor); @@ -1173,23 +1278,23 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, fogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, fogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { fogColor = exponentialHeightFog.xyz; - fogColor *= GetWaterFogFade(); + fogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, fogColor, exponentialHeightFog.w); } else { - fogColor *= GetWaterFogFade(); + fogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); - float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(); + float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, expFogColor, exponentialHeightFog.w); } } else { - fogColor *= GetWaterFogFade(); + fogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); } # else - fogColor *= GetWaterFogFade(); + fogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, fogColor, fogDistanceFactor); # endif @@ -1224,23 +1329,23 @@ PS_OUTPUT main(PS_INPUT input) # endif # if defined(EXP_HEIGHT_FOG) if (SharedData::exponentialHeightFogSettings.enabled) { - float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust.xyz, preFogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); + float4 exponentialHeightFog = ExponentialHeightFog::GetExponentialHeightFog(input.WPosition.xyz, FrameBuffer::CameraPosAdjust[eyeIndex].xyz, preFogColor, float4(input.HPosition.xy * FrameBuffer::DynamicResolutionParams2.xy, input.HPosition.z, 1)); if (ExponentialHeightFog::ShouldDisableVanillaFog()) { preFogColor = exponentialHeightFog.xyz; - preFogColor *= GetWaterFogFade(); + preFogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, preFogColor, exponentialHeightFog.w); } else { - preFogColor *= GetWaterFogFade(); + preFogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); - float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(); + float3 expFogColor = exponentialHeightFog.xyz * GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, expFogColor, exponentialHeightFog.w); } } else { - preFogColor *= GetWaterFogFade(); + preFogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); } # else - preFogColor *= GetWaterFogFade(); + preFogColor *= GetWaterFogFade(eyeIndex); finalColorPreFog = lerp(finalColorPreFog, preFogColor, fogDistanceFactor); # endif diff --git a/src/CSEditor/Weather/PrecipitationWidget.cpp b/src/CSEditor/Weather/PrecipitationWidget.cpp index 17cca25148..8efa1d915a 100644 --- a/src/CSEditor/Weather/PrecipitationWidget.cpp +++ b/src/CSEditor/Weather/PrecipitationWidget.cpp @@ -235,7 +235,7 @@ void PrecipitationWidget::LoadFromGameSettings() settings.particleType = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleType).i; settings.boxSize = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kBoxSize).f; settings.particleDensity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; - auto& particleTexture = precipitation->GetRuntimeData().particleTexture; + GET_INSTANCE_MEMBER(particleTexture, precipitation) settings.particleTexture = particleTexture.textureName.c_str(); } @@ -276,7 +276,7 @@ void PrecipitationWidget::ApplyChanges() precipitation->GetSettingRef(DataID::kParticleType).i = settings.particleType; precipitation->GetSettingRef(DataID::kBoxSize).f = settings.boxSize; precipitation->GetSettingRef(DataID::kParticleDensity).f = settings.particleDensity; - auto& particleTexture = precipitation->GetRuntimeData().particleTexture; + GET_INSTANCE_MEMBER(particleTexture, precipitation) particleTexture.textureName = settings.particleTexture.c_str(); ApplyLiveParticleTexture(settings.particleTexture); Widget::ForceCurrentWeatherReinit(); diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 711ba9f2cd..ab817c33d6 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -14,6 +14,7 @@ #include "Features/SubsurfaceScattering.h" #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" +#include "Features/VR.h" #include "Features/CSEditor.h" #include "Hooks.h" @@ -228,9 +229,9 @@ void Deferred::StartDeferred() globals::state->UpdateSharedData(true, false); auto shadowState = globals::game::shadowState; - auto& renderTargets = shadowState->GetRuntimeData().renderTargets; - auto& setRenderTargetMode = shadowState->GetRuntimeData().setRenderTargetMode; - auto& stateUpdateFlags = shadowState->GetRuntimeData().stateUpdateFlags; + GET_INSTANCE_MEMBER(renderTargets, shadowState) + GET_INSTANCE_MEMBER(setRenderTargetMode, shadowState) + GET_INSTANCE_MEMBER(stateUpdateFlags, shadowState) // Backup original render targets for (uint i = 0; i < 4; i++) { @@ -260,6 +261,10 @@ void Deferred::StartDeferred() { auto context = globals::d3d::context; + // Clear POM offset texture to -1.0 sentinel so pixels the Lighting PS never touches read "no POM" + if (globals::features::vr.stereoOpt.loaded) + globals::features::vr.stereoOpt.ClearPomOffsetTexture(); + ID3D11Buffer* buffers[1] = { *globals::game::perFrame.get() }; ID3D11Buffer* vrBuffer = nullptr; @@ -279,6 +284,12 @@ void Deferred::StartDeferred() PrepassPasses(); OverrideBlendStates(); + + // VR: Classify Eye 1 pixels and write hardware stencil marks before geometry rendering. + // Only enable stencil culling when overwrite reprojection is available for this frame. + if (globals::game::isVR && globals::features::vr.IsStereoOptimizationCullingReady()) { + globals::features::vr.stereoOpt.DispatchStencil(); + } } void Deferred::DeferredPasses() @@ -368,6 +379,14 @@ void Deferred::DeferredPasses() context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); + // Bind VRStereoOptimizations mode texture for Eye 1 skip. + // Bind null when disabled so stale mode data doesn't cause incorrect early-exits + // in DeferredCompositeCS (null SRV reads return 0 = MODE_DISOCCLUDED, all pixels composite normally). + auto& vrStereoOpt = globals::features::vr.stereoOpt; + bool stereoCullingReady = globals::features::vr.IsStereoOptimizationCullingReady(); + ID3D11ShaderResourceView* modeSRV = stereoCullingReady ? vrStereoOpt.GetModeTextureSRV() : nullptr; + context->CSSetShaderResources(16, 1, &modeSRV); + ID3D11UnorderedAccessView* uavs[3]{ main.UAV, normals.UAV, motionVectors.UAV }; context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); @@ -380,6 +399,27 @@ void Deferred::DeferredPasses() context->Dispatch(dispatchCount.x, dispatchCount.y, 1); globals::profiler->EndPass(); } + + // Unbind mode texture SRV + ID3D11ShaderResourceView* nullSRV = nullptr; + context->CSSetShaderResources(16, 1, &nullSRV); + } + + // VR: Deactivate stencil culling now that geometry rendering is complete. + // Must happen before StereoBlend so the blend pass itself isn't stencil-blocked. + if (globals::game::isVR) { + auto& stereoOpt = globals::features::vr.stereoOpt; + if (stereoOpt.IsStencilActive()) { + stereoOpt.DeactivateStencil(); + } + } + + // VR: Stereo reprojection fills Eye 1 holes here (after DeferredComposite, before SSR/water/sky) + // so that ISReflectionsRayTracing sees valid pixels in both eyes. + if (globals::game::isVR) { + globals::profiler->BeginPass("VR::StereoBlend"); + globals::features::vr.DrawStereoBlend(); + globals::profiler->EndPass(); } // Clear @@ -411,8 +451,8 @@ void Deferred::EndDeferred() return; auto shadowState = globals::game::shadowState; - auto& renderTargets = shadowState->GetRuntimeData().renderTargets; - auto& stateUpdateFlags = shadowState->GetRuntimeData().stateUpdateFlags; + GET_INSTANCE_MEMBER(renderTargets, shadowState) + GET_INSTANCE_MEMBER(stateUpdateFlags, shadowState) // Do not render to our targets past this point for (uint i = 0; i < 4; i++) { @@ -581,7 +621,10 @@ void Deferred::CopyShadowLightData() dd.EndSplitDistances = { dirData.endSplitDistances[0], dirData.endSplitDistances[1] }; dd.StartSplitDistances = { dirData.startSplitDistances[0], dirData.startSplitDistances[1] }; - SetShadowCascadeParameters(sunShadowLight->GetRuntimeData(), dd); + if (globals::game::isVR) + SetShadowCascadeParameters(sunShadowLight->GetVRRuntimeData(), dd); + else + SetShadowCascadeParameters(sunShadowLight->GetRuntimeData(), dd); D3D11_MAPPED_SUBRESOURCE mapped{}; DX::ThrowIfFailed(context->Map(directionalShadowLights->resource.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)); diff --git a/src/Deferred.h b/src/Deferred.h index a2d6a8f4c0..e032878f8b 100644 --- a/src/Deferred.h +++ b/src/Deferred.h @@ -149,11 +149,11 @@ class Deferred { stl::write_vfunc<0x35, BSCubeMapCamera_RenderCubemap>(RE::VTABLE_BSCubeMapCamera[0]); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x2EC, 0x2EC)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x2EC, 0x2EC, 0x248)); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x831, 0x841)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x831, 0x841, 0x791)); stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x8E, 0x84)); - stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x319, 0x308)); + stl::write_thunk_call(REL::RelocationID(99938, 106583).address() + REL::Relocate(0x319, 0x308, 0x321)); if (!globals::game::isVR) stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x944, 0x954)); diff --git a/src/EngineFixes/ShadowmapCascadeCullingFix.cpp b/src/EngineFixes/ShadowmapCascadeCullingFix.cpp index 1b537a3ac7..aed49dde1d 100644 --- a/src/EngineFixes/ShadowmapCascadeCullingFix.cpp +++ b/src/EngineFixes/ShadowmapCascadeCullingFix.cpp @@ -4,7 +4,7 @@ void ShadowmapCascadeCullingFix::Install() { gfSplitOverlap = reinterpret_cast(REL::RelocationID(513805, 391863).address()); - stl::write_thunk_call(REL::RelocationID(101499, 108496).address() + REL::Relocate(0x1B12, 0x1C02)); + stl::write_thunk_call(REL::RelocationID(101499, 108496).address() + REL::Relocate(0x1B12, 0x1C02, 0x1C82)); } void ShadowmapCascadeCullingFix::BSShadowDirectionalLight_SetFrameCamera_BuildCascadeCameraCullingPlanes::thunk(RE::BSShadowDirectionalLight* dirLight, RE::NiFrustumPlanes& outPlanes, FrustumSplit& frustumSplit, uint32_t splitCornerIndices[8], uint32_t numSplitCornerIndices, RE::NiPoint3& lightDir, RE::NiPoint3& cameraPos, uint32_t cornerOffsetIndex) diff --git a/src/EngineFixes/ShadowmapCascadeRasterizerFix.h b/src/EngineFixes/ShadowmapCascadeRasterizerFix.h index cd6504e919..357e5fbf8f 100644 --- a/src/EngineFixes/ShadowmapCascadeRasterizerFix.h +++ b/src/EngineFixes/ShadowmapCascadeRasterizerFix.h @@ -68,6 +68,6 @@ struct ShadowmapRasterizerFix : EngineFix "Controls the number of shadow map cascades used for directional lighting. " "Higher values provide better shadow quality but use more GPU resources. " "Maximum of 3 cascades supported. ", - static_cast(0), 2, 1, 3 } }, + REL::Relocate(0, 0, 0x1ed6350), 2, 1, 3 } }, }; }; diff --git a/src/Feature.cpp b/src/Feature.cpp index e03777a651..7bdf647aeb 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -34,6 +34,7 @@ #include "Features/TerrainVariation.h" #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" +#include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" diff --git a/src/Feature.h b/src/Feature.h index 4fcce37852..a94dcaed9e 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -89,6 +89,12 @@ struct Feature public: virtual bool HasShaderDefine(RE::BSShader::Type) { return false; } + /** + * Whether the feature supports VR. + * + * \return true if VR supported; else false + */ + virtual bool SupportsVR() { return false; } /** * Whether the feature is a CORE feature diff --git a/src/FeatureConstraints.h b/src/FeatureConstraints.h index 23a0a3cf6b..8437025f5b 100644 --- a/src/FeatureConstraints.h +++ b/src/FeatureConstraints.h @@ -11,7 +11,7 @@ namespace FeatureConstraints */ struct SettingId { - std::string featureShortName; + std::string featureShortName; // e.g., "VR" std::string settingPath; // e.g., "EnableDepthBufferCullingExterior" bool operator==(const SettingId& other) const diff --git a/src/FeatureIssues.h b/src/FeatureIssues.h index 80ad8bab3b..ce6103cdd5 100644 --- a/src/FeatureIssues.h +++ b/src/FeatureIssues.h @@ -165,7 +165,7 @@ namespace FeatureIssues * * This function scans the Data/Shaders/Features/ directory for INI files that * correspond to features not currently in the active feature list (e.g., obsolete - * features, unknown features). It identifies whether + * features, VR features in non-VR mode, unknown features). It identifies whether * these orphaned INI files are known obsolete features or completely unknown features * and adds them to the feature issues tracking system. * diff --git a/src/Features/CSEditor.cpp b/src/Features/CSEditor.cpp index 70d87849c4..485386f731 100644 --- a/src/Features/CSEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -493,7 +493,7 @@ void CSEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) } auto particleDensity = weather->precipitationData->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; ImGui::BulletText(T(TKEY("particle_density"), "Particle Density: %.3f"), particleDensity); - auto& particleTexture = weather->precipitationData->GetRuntimeData().particleTexture; + GET_INSTANCE_MEMBER(particleTexture, weather->precipitationData) if (!particleTexture.textureName.empty()) { ImGui::BulletText(T(TKEY("particle_texture"), "Particle Texture: %s"), particleTexture.textureName.c_str()); } else { diff --git a/src/Features/CSEditor.h b/src/Features/CSEditor.h index 0841dec4f1..554731c393 100644 --- a/src/Features/CSEditor.h +++ b/src/Features/CSEditor.h @@ -20,6 +20,7 @@ struct CSEditor : OverlayFeature virtual inline std::string GetShortName() override { return "CSEditor"; } virtual inline std::string_view GetShaderDefineName() override { return "CS_EDITOR"; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kUtility; } + virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } virtual bool IsInMenu() const override { return true; } diff --git a/src/Features/CloudShadows.cpp b/src/Features/CloudShadows.cpp index 2a3d5392d8..fe958a0ce1 100644 --- a/src/Features/CloudShadows.cpp +++ b/src/Features/CloudShadows.cpp @@ -99,7 +99,7 @@ void CloudShadows::ModifySky(RE::BSRenderPass* Pass) { auto shadowState = globals::game::shadowState; - auto& cubeMapRenderTarget = shadowState->GetRuntimeData().cubeMapRenderTarget; + GET_INSTANCE_MEMBER(cubeMapRenderTarget, shadowState); if (cubeMapRenderTarget != RE::RENDER_TARGETS_CUBEMAP::kREFLECTIONS) return; diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index 0e75f6cb03..f25123a035 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -74,4 +74,5 @@ struct CloudShadows : Feature logger::info("[Cloud Shadows] Installed hooks"); } }; + virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 2c1c873bac..f18f961b2d 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -339,7 +339,7 @@ void DynamicCubemaps::UpdateCubemapCapture(bool a_reflections) static float3 cameraPreviousPosAdjust[2] = { { 0, 0, 0 }, { 0, 0, 0 } }; updateData.CameraPreviousPosAdjust = cameraPreviousPosAdjust[index]; - auto eyePosition = Util::GetEyePosition(); + auto eyePosition = Util::GetEyePosition(0); cameraPreviousPosAdjust[index] = { eyePosition.x, eyePosition.y, eyePosition.z }; diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index fbb2d8ff59..1f09741e7a 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -143,7 +143,8 @@ struct DynamicCubemaps : Feature { T("feature.dynamic_cubemaps.key_feature_1", "Real-time environment capture for realistic reflections"), T("feature.dynamic_cubemaps.key_feature_2", "Dynamic cube map generation based on camera position"), T("feature.dynamic_cubemaps.key_feature_3", "Enhanced water reflections with environmental details"), - T("feature.dynamic_cubemaps.key_feature_4", "Optimized cubemap inference and irradiance calculation") } }; + T("feature.dynamic_cubemaps.key_feature_4", "Support for both standard and VR rendering modes"), + T("feature.dynamic_cubemaps.key_feature_5", "Optimized cubemap inference and irradiance calculation") } }; }; virtual std::vector> GetShaderDefineOptions() override; @@ -161,6 +162,20 @@ struct DynamicCubemaps : Feature virtual void DataLoaded() override; virtual void PostPostLoad() override; + std::map iniVRCubeMapSettings{ + { "bAutoWaterSilhouetteReflections:Water", { "Auto Water Silhouette Reflections", "Automatically reflects silhouettes on water surfaces.", 0, true, false, true } }, + { "bForceHighDetailReflections:Water", { "Force High Detail Reflections", "Forces the use of high-detail reflections on water surfaces.", 0, true, false, true } } + }; + + std::map hiddenVRCubeMapSettings{ + { "bReflectExplosions:Water", { "Reflect Explosions", "Enables reflection of explosions on water surfaces.", 0x1eaa000, true, false, true } }, + { "bReflectLODLand:Water", { "Reflect LOD Land", "Enables reflection of low-detail (LOD) terrain on water surfaces.", 0x1eaa060, true, false, true } }, + { "bReflectLODObjects:Water", { "Reflect LOD Objects", "Enables reflection of low-detail (LOD) objects on water surfaces.", 0x1eaa078, true, false, true } }, + { "bReflectLODTrees:Water", { "Reflect LOD Trees", "Enables reflection of low-detail (LOD) trees on water surfaces.", 0x1eaa090, true, false, true } }, + { "bReflectSky:Water", { "Reflect Sky", "Enables reflection of the sky on water surfaces.", 0x1eaa0a8, true, false, true } }, + { "bUseWaterRefractions:Water", { "Use Water Refractions", "Enables refractions for water surfaces, affecting how light bends through water.", 0x1eaa0c0, true, false, true } } + }; + virtual void ClearShaderCache() override; ID3D11ComputeShader* GetComputeShaderUpdate(); ID3D11ComputeShader* GetComputeShaderUpdateReflections(); @@ -182,5 +197,6 @@ struct DynamicCubemaps : Feature ID3D11ComputeShader* GetComputeShaderBC6HEncode(); + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/ExponentialHeightFog.cpp b/src/Features/ExponentialHeightFog.cpp index 64108cdef1..4eb4c1cb9d 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -211,8 +211,7 @@ void ExponentialHeightFog::EnsureVolumetricResources() { uint32_t pixelSize = std::clamp(settings.volumetricGridPixelSize, 4u, 64u); const uint32_t gridZ = std::clamp(settings.volumetricGridSizeZ, 16u, 160u); - float2 screenSz{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; - auto renderSize = Util::ConvertToDynamic(screenSz); + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); auto getGridSize = [&renderSize, gridZ](uint32_t a_pixelSize) { return DirectX::XMUINT4{ @@ -441,7 +440,13 @@ void ExponentialHeightFog::Prepass() 0.0f }; - cb.clipToWorld = globals::game::frameBufferCached.GetCameraViewProjUnjittered().Invert(); + const uint32_t eyeCount = globals::game::isVR ? 2u : 1u; + for (uint32_t eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { + cb.clipToWorld[eyeIndex] = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Invert(); + } + if (eyeCount == 1u) { + cb.clipToWorld[1] = cb.clipToWorld[0]; + } for (uint32_t i = 0; i < std::size(cb.frameJitterOffsets); i++) { const uint32_t temporalFrame = (globals::state->frameCount - i) & 1023u; diff --git a/src/Features/ExponentialHeightFog.h b/src/Features/ExponentialHeightFog.h index 93b032b591..b386df78e5 100644 --- a/src/Features/ExponentialHeightFog.h +++ b/src/Features/ExponentialHeightFog.h @@ -8,6 +8,7 @@ struct ExponentialHeightFog : Feature static constexpr std::string_view MOD_ID = "180146"; public: + virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Exponential Height Fog"; } virtual std::string GetDisplayName() override { return T("feature.exponential_height_fog.name", "Exponential Height Fog"); } virtual inline std::string GetShortName() override { return "ExponentialHeightFog"; } @@ -83,7 +84,7 @@ struct ExponentialHeightFog : Feature DirectX::XMUINT4 gridSizeAndFlags = {}; float4 invGridSizeAndNearFade = {}; float4 gridZParams = {}; - float4x4 clipToWorld = {}; + float4x4 clipToWorld[2] = {}; float4 frameJitterOffsets[16] = {}; float4 historyParameters = {}; float4 jitterParameters = {}; // x = LightScatteringSampleJitterMultiplier, y = StateFrameIndexMod8, zw = unused diff --git a/src/Features/ExtendedMaterials.h b/src/Features/ExtendedMaterials.h index 7254daf12e..0f3bd19a4d 100644 --- a/src/Features/ExtendedMaterials.h +++ b/src/Features/ExtendedMaterials.h @@ -49,5 +49,6 @@ struct ExtendedMaterials : Feature virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/ExtendedTranslucency.h b/src/Features/ExtendedTranslucency.h index 61706c0379..fd40a8f0cb 100644 --- a/src/Features/ExtendedTranslucency.h +++ b/src/Features/ExtendedTranslucency.h @@ -25,6 +25,7 @@ struct ExtendedTranslucency final : Feature virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; static void BSLightingShader_SetupGeometry(RE::BSRenderPass* pass); diff --git a/src/Features/GrassCollision.cpp b/src/Features/GrassCollision.cpp index 5681646666..d23ad705ce 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -40,7 +40,7 @@ void GrassCollision::QueueCollisions() return; eastl::vector actorCandidates{}; - RE::NiPoint3 cameraPosition = Util::GetEyePosition(); + RE::NiPoint3 cameraPosition = Util::GetEyePosition(0); auto addActorCandidate = [&](RE::ActorHandle a_handle) { auto actor = a_handle.get(); @@ -160,7 +160,7 @@ void GrassCollision::Update() static float2 prevCellID = { 0, 0 }; - auto eyePosNI = Util::GetEyePosition(); + auto eyePosNI = Util::GetEyePosition(0); static auto prevEyePosNI = eyePosNI; auto eyePos = float2{ eyePosNI.x, eyePosNI.y }; diff --git a/src/Features/GrassCollision.h b/src/Features/GrassCollision.h index e133077b71..da25c5e150 100644 --- a/src/Features/GrassCollision.h +++ b/src/Features/GrassCollision.h @@ -86,6 +86,7 @@ struct GrassCollision : Feature virtual void PostPostLoad() override; + virtual bool SupportsVR() override { return true; }; struct Hooks { @@ -104,7 +105,7 @@ struct GrassCollision : Feature static void Install() { stl::write_vfunc<0x6, BSGrassShader_SetupGeometry>(RE::VTABLE_BSGrassShader[0]); - stl::write_thunk_call(REL::RelocationID(35565, 36564).address() + REL::Relocate(0x748, 0xC26)); + stl::write_thunk_call(REL::RelocationID(35565, 36564).address() + REL::Relocate(0x748, 0xC26, 0x7EE)); logger::info("[GRASS COLLISION] Installed hooks"); } }; diff --git a/src/Features/GrassLighting.h b/src/Features/GrassLighting.h index 1bece2fb1f..61bacc67ec 100644 --- a/src/Features/GrassLighting.h +++ b/src/Features/GrassLighting.h @@ -43,4 +43,5 @@ struct GrassLighting : Feature virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 545b0a950b..086129f98e 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -229,6 +229,26 @@ namespace func(a_this, a3, a_target, a_4, a_5); hdr->RestoreFramebuffer(); + // VR: RedirectFramebuffer made ISHDR write to hdrTexture (float16); after + // RestoreFramebuffer kFRAMEBUFFER reverts to its original texture. + // ISCopy reads kFRAMEBUFFER.SRV to distribute the frame to the HMD and + // companion window, so we must write the tonemapped content back into + // kFRAMEBUFFER before ISCopy runs. + // + // TODO (future HDR HMD support): The correct pipeline is to run the full + // HDR composite (PQ encode, paper white, peak nits) HERE, writing the + // result back to kFRAMEBUFFER so ISCopy distributes HDR-processed content + // to both the HMD and companion at their native sizes. The post-Present + // ApplyHDR path cannot do this correctly because ISCopy has already run + // and the companion back buffer (1024x1024) does not match outputTexture + // (sized from kMAIN). Requires hooking the ISCopy vfunc to fire + // HDROutputCS before distribution. + if (globals::game::isVR && hdr->settings.enableHDR && + hdr->hdrTexture && hdr->hdrTexture->resource) { + auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; + if (fb.texture) + globals::d3d::context->CopyResource(fb.texture, hdr->hdrTexture->resource.get()); + } } static inline REL::Relocation func; }; @@ -606,7 +626,7 @@ void HDRDisplay::PostPostLoad() if (!globals::features::upscaling.loaded) { logger::info("[HDR Display] Installing HDR pipeline hooks (Upscaling not loaded)"); stl::detour_thunk(REL::RelocationID(79947, 82084)); - stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7)); + stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7, 0x206)); } } @@ -848,6 +868,13 @@ bool HDRDisplay::ShouldUseD3D12UIBuffer() void HDRDisplay::SetUIBuffer() { + // VR: ISCopy reads kFRAMEBUFFER.SRV to distribute the frame to the HMD and + // companion window. Redirecting kFRAMEBUFFER.RTV here would cause vanilla UI + // to render into uiTexture instead, so ISCopy would send a UI-less frame to + // the HMD. Leave kFRAMEBUFFER alone; vanilla UI bakes directly into it. + if (globals::game::isVR) + return; + auto& fb = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER]; // D3D12 swap chain path: route UI to uiBufferWrapped only when a compositor @@ -909,7 +936,7 @@ void HDRDisplay::SetUIBuffer() bool HDRDisplay::UsesDeferredPresentComposite() const { - return loaded && settings.enableHDR && + return loaded && settings.enableHDR && !globals::game::isVR && !globals::features::upscaling.d3d12SwapChainActive && uiTexture && uiTexture->rtv && hdrOutputCS; } @@ -950,7 +977,7 @@ namespace { static void WINAPI thunk(ID3D11DeviceContext* This, ID3D11BlendState* pBlendState, const FLOAT BlendFactor[4], UINT SampleMask) { - if (pBlendState) { + if (pBlendState && !globals::game::isVR) { auto& hdr = globals::features::hdrDisplay; const bool d3d11HdrCapture = hdr.loaded && hdr.settings.enableHDR && hdr.uiTexture; const bool fgCapture = globals::features::upscaling.d3d12SwapChainActive; @@ -1030,7 +1057,7 @@ void HDRDisplay::DrawImGuiForPresent(bool frameGenActive, bool hdrReady) if (frameGenActive) { auto& data = globals::game::renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGET::kFRAMEBUFFER]; globals::d3d::context->OMSetRenderTargets(1, &data.RTV, nullptr); - } else if (hdrReady && uiTexture && uiTexture->rtv && uiTexture->resource) { + } else if (hdrReady && !globals::game::isVR && uiTexture && uiTexture->rtv && uiTexture->resource) { ID3D11RenderTargetView* uiRTV = uiTexture->rtv.get(); D3D11_TEXTURE2D_DESC texDesc{}; uiTexture->resource->GetDesc(&texDesc); @@ -1169,18 +1196,27 @@ void HDRDisplay::ApplyHDR() auto& framebufferRT = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; // Scene SRV selection: - // - HDR: hdrTexture has float16 scene values >1.0 preserved from ISHDR. - // - SDR: kFRAMEBUFFER has the tonemapped 0-1 ISHDR output. + // - VR: kFRAMEBUFFER at this point has scene + vanilla UI + ImGui all baked in + // (thunk restored hdrTexture→kFRAMEBUFFER, vanilla UI rendered on top, ImGui + // just rendered to kFRAMEBUFFER.RTV above). Use it directly so the companion + // window gets everything without a separate uiTexture capture pass. + // - Non-VR HDR: hdrTexture has float16 scene values >1.0 preserved from ISHDR. + // - Non-VR SDR: kFRAMEBUFFER has the tonemapped 0-1 ISHDR output. ID3D11ShaderResourceView* sceneSRV = + globals::game::isVR ? framebufferRT.SRV : (settings.enableHDR && hdrTexture && hdrTexture->srv) ? hdrTexture->srv.get() : framebufferRT.SRV; // Choose the correct UI buffer based on which path is active. + // VR uses the framebuffer directly, which already contains vanilla UI/ImGui. + // Binding a separate uiTexture here would duplicate the UI layer. ID3D11ShaderResourceView* uiSRV = nullptr; - if (upscaling.d3d12SwapChainActive && upscaling.dx12SwapChain.uiBufferWrapped) { - uiSRV = upscaling.dx12SwapChain.uiBufferWrapped->srv; - } else if (uiTexture && uiTexture->srv) { - uiSRV = uiTexture->srv.get(); + if (!globals::game::isVR) { + if (upscaling.d3d12SwapChainActive && upscaling.dx12SwapChain.uiBufferWrapped) { + uiSRV = upscaling.dx12SwapChain.uiBufferWrapped->srv; + } else if (uiTexture && uiTexture->srv) { + uiSRV = uiTexture->srv.get(); + } } ID3D11ShaderResourceView* views[2] = { sceneSRV, uiSRV }; diff --git a/src/Features/HDRDisplay.h b/src/Features/HDRDisplay.h index 41771236b8..e5cc29b58f 100644 --- a/src/Features/HDRDisplay.h +++ b/src/Features/HDRDisplay.h @@ -20,6 +20,7 @@ struct HDRDisplay : public Feature virtual inline std::string GetShortName() override { return "HDRDisplay"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } virtual inline std::string_view GetCategory() const override { return "Display"; } + virtual inline bool SupportsVR() override { return false; } virtual inline bool IsCore() const override { return false; } virtual inline std::string_view GetShaderDefineName() override { return "HDR_OUTPUT"; } diff --git a/src/Features/HairSpecular.h b/src/Features/HairSpecular.h index 147586e42b..28fab821ec 100644 --- a/src/Features/HairSpecular.h +++ b/src/Features/HairSpecular.h @@ -59,4 +59,5 @@ struct HairSpecular : Feature virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; }; \ No newline at end of file diff --git a/src/Features/IBL.h b/src/Features/IBL.h index bef866d50f..5b153208e4 100644 --- a/src/Features/IBL.h +++ b/src/Features/IBL.h @@ -3,6 +3,7 @@ struct IBL : Feature { public: + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; virtual inline std::string GetName() override { return "Image Based Lighting"; } diff --git a/src/Features/InteriorSun.cpp b/src/Features/InteriorSun.cpp index 3bb494d6bc..ca921845b9 100644 --- a/src/Features/InteriorSun.cpp +++ b/src/Features/InteriorSun.cpp @@ -51,9 +51,9 @@ void InteriorSun::PostPostLoad() stl::write_thunk_call(REL::RelocationID(100852, 107642).address() + REL::Relocate(0x29E, 0x28F)); // Hooks and patch to enable directional lighting for interiors - stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x399, 0x37D)); - stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x3AE, 0x392)); - REL::safe_fill(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x397, 0x37B), REL::NOP, 2); + stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x399, 0x37D, 0x639)); + stl::write_thunk_call(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x3AE, 0x392, 0x64E)); + REL::safe_fill(REL::RelocationID(35562, 36561).address() + REL::Relocate(0x397, 0x37B, 0x637), REL::NOP, 2); // Hook for overriding the rooms and portals passed to the directional light culling step to fix light leaking through unrendered geometry stl::detour_thunk(REL::RelocationID(101498, 108492)); @@ -67,11 +67,11 @@ void InteriorSun::PostPostLoad() gInteriorShadowDistance = reinterpret_cast(REL::RelocationID(513755, 391724).address()); // Patches BSShadowDirectionalLight::SetFrameCamera to read the correct shadow distance value in interior cells - const std::uintptr_t address = REL::RelocationID(101499, 108496).address() + REL::Relocate(0xD62, 0xE6C); + const std::uintptr_t address = REL::RelocationID(101499, 108496).address() + REL::Relocate(0xD62, 0xE6C, 0xE72); const std::int32_t displacement = static_cast(reinterpret_cast(gShadowDistance) - (address + 8)); REL::safe_write(address + 4, &displacement, sizeof(displacement)); - rasterStateCullMode = &globals::game::shadowState->GetRuntimeData().rasterStateCullMode; + rasterStateCullMode = globals::game::isVR ? &globals::game::shadowState->GetVRRuntimeData().rasterStateCullMode : &globals::game::shadowState->GetRuntimeData().rasterStateCullMode; logger::info("[Interior Sun] Installed hooks"); } diff --git a/src/Features/InteriorSun.h b/src/Features/InteriorSun.h index 11f2118ae6..28918e6769 100644 --- a/src/Features/InteriorSun.h +++ b/src/Features/InteriorSun.h @@ -21,6 +21,7 @@ struct InteriorSun : Feature virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; virtual void EarlyPrepass() override; diff --git a/src/Features/InverseSquareLighting.h b/src/Features/InverseSquareLighting.h index 303ed4bc92..37a56dfb70 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -25,6 +25,7 @@ struct InverseSquareLighting : Feature inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }; + virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/LODBlending.h b/src/Features/LODBlending.h index 0b4c1b1415..9eb866ad02 100644 --- a/src/Features/LODBlending.h +++ b/src/Features/LODBlending.h @@ -40,5 +40,6 @@ struct LODBlending : Feature virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/LightLimitFix.cpp b/src/Features/LightLimitFix.cpp index e3a2638db7..78013aa874 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -732,7 +732,8 @@ void LightLimitFix::BSLightingShader_SetupGeometry_After(RE::BSRenderPass*) void LightLimitFix::SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPoint3 a_initialPosition, bool a_cached) { - RE::NiPoint3 eyePosition; + for (int eyeIndex = 0; eyeIndex < eyeCount; eyeIndex++) { + RE::NiPoint3 eyePosition; if (a_cached) eyePosition = eyePositionCached[eyeIndex]; @@ -744,11 +745,6 @@ void LightLimitFix::SetLightPosition(LightLimitFix::LightData& a_light, RE::NiPo a_light.positionWS[eyeIndex].data.y = worldPos.y; a_light.positionWS[eyeIndex].data.z = worldPos.z; } - - auto worldPos = a_initialPosition - eyePosition; - a_light.positionWS.data.x = worldPos.x; - a_light.positionWS.data.y = worldPos.y; - a_light.positionWS.data.z = worldPos.z; } void LightLimitFix::RefreshJsonPlacedLightCacheFrame() diff --git a/src/Features/LightLimitFix.h b/src/Features/LightLimitFix.h index 4785fff802..de52e2f058 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -66,7 +66,7 @@ struct LightLimitFix : OverlayFeature float invRadius; float fadeZone; float sizeBias; - PositionOpt positionWS; + PositionOpt positionWS[2]; uint128_t roomFlags = uint32_t(0); stl::enumeration lightFlags; uint32_t shadowMapIndex = 0; @@ -223,7 +223,7 @@ struct LightLimitFix : OverlayFeature float lightsNear = 1; float lightsFar = 16384; - RE::NiPoint3 eyePositionCached{}; + RE::NiPoint3 eyePositionCached[2]{}; bool wasEmpty = false; bool wasWorld = false; int previousRoomIndex = -1; @@ -422,13 +422,14 @@ struct LightLimitFix : OverlayFeature stl::write_vfunc<0x6, BSWaterShader_SetupGeometry>(RE::VTABLE_BSWaterShader[0]); stl::write_thunk_call(REL::RelocationID(100994, 107781).address() + 0x92); - stl::write_thunk_call(REL::RelocationID(100997, 107784).address() + REL::Relocate(0x139, 0x12A)); + stl::write_thunk_call(REL::RelocationID(100997, 107784).address() + REL::Relocate(0x139, 0x12A, 0x133)); stl::write_thunk_call(REL::RelocationID(101296, 108283).address() + REL::Relocate(0xB7, 0x7E)); logger::info("[LLF] Installed hooks"); } }; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; } }; @@ -458,10 +459,10 @@ struct fmt::formatter auto format(const LightLimitFix::LightData& l, format_context& ctx) const -> format_context::iterator { // ctx.out() is an output iterator to write to. - return fmt::format_to(ctx.out(), "{{address {:x} color {} radius {} posWS {}}}", + return fmt::format_to(ctx.out(), "{{address {:x} color {} radius {} posWS {} {}}}", reinterpret_cast(&l), (Vector3)l.color, l.radius, - (Vector3)l.positionWS.data); + (Vector3)l.positionWS[0].data, (Vector3)l.positionWS[1].data); } }; diff --git a/src/Features/LinearLighting.cpp b/src/Features/LinearLighting.cpp index ffed1a9d81..ce3ead1a31 100644 --- a/src/Features/LinearLighting.cpp +++ b/src/Features/LinearLighting.cpp @@ -117,7 +117,7 @@ void LinearLighting::Prepass() if (!imageSpaceManager) return; - dirLightMult = imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale; + dirLightMult = !globals::game::isVR ? imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale : imageSpaceManager->GetVRRuntimeData().data.baseData.hdr.sunlightScale; } struct LinearLighting::Hooks diff --git a/src/Features/LinearLighting.h b/src/Features/LinearLighting.h index 2195154192..a3e30ade1c 100644 --- a/src/Features/LinearLighting.h +++ b/src/Features/LinearLighting.h @@ -20,6 +20,7 @@ struct LinearLighting : Feature T("feature.linear_lighting.key_feature_3", "Makes PBR really work") } }; }; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; struct Settings diff --git a/src/Features/PerformanceOverlay.h b/src/Features/PerformanceOverlay.h index e3e3872bda..d09ba40818 100644 --- a/src/Features/PerformanceOverlay.h +++ b/src/Features/PerformanceOverlay.h @@ -123,6 +123,7 @@ struct PerformanceOverlay : OverlayFeature std::string GetName() override { return "Performance Overlay"; } virtual std::string GetDisplayName() override { return T("feature.performance_overlay.name", "Performance Overlay"); } std::string GetShortName() override { return "PerformanceOverlay"; } + virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } virtual bool IsInMenu() const override { return true; } bool IsOverlayVisible() const override { return settings.ShowInOverlay; } diff --git a/src/Features/RenderDoc.h b/src/Features/RenderDoc.h index 32844a2494..6178c62757 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -63,6 +63,7 @@ class RenderDoc : public Feature T("feature.render_doc.key_feature_2", "Open captures folder"), T("feature.render_doc.key_feature_3", "Capture file management") } }; } + bool SupportsVR() override { return true; } std::string_view GetShaderDefineName() override { return ""; } bool HasShaderDefine(RE::BSShader::Type) override { return false; }; diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 6fa4bf490b..84d837f2e1 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -69,9 +69,13 @@ void ScreenSpaceGI::DrawSettings() } ImGui::TableNextColumn(); { + auto vanillaSSAOGuard = Util::DisableGuard(globals::game::isVR); ImGui::Checkbox(T(TKEY("vanilla_ssao"), "Vanilla SSAO"), &settings.EnableVanillaSSAO); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip"), "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.")); + if (globals::game::isVR) + ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip_vr"), "Vanilla SSAO is not supported in VR.")); + else + ImGui::Text("%s", T(TKEY("vanilla_ssao_tooltip"), "Enable Skyrim's built-in SSAO. Usually disabled when using SSGI to avoid double-darkening.")); } } ImGui::TableNextColumn(); @@ -91,16 +95,18 @@ void ScreenSpaceGI::DrawSettings() auto qualityGuard = Util::DisableGuard(!settings.Enabled); if (ImGui::BeginTable("Presets", 5)) { + auto select = [](auto flatVal, auto vrVal) { return globals::game::isVR ? vrVal : flatVal; }; + ImGui::TableNextColumn(); if (ImGui::Button(T(TKEY("ao_only"), "AO only"), { -1, 0 })) { - settings.NumSlices = 1; - settings.NumSteps = 6; + settings.NumSlices = select(1, 3); + settings.NumSteps = select(6, 8); settings.EnableBlur = true; settings.EnableGI = false; recompileFlag = true; } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("1 Slice, 6 Steps, blur enabled, no GI\n"); + ImGui::Text(select("1 Slice, 6 Steps, blur enabled, no GI\n", "3 Slices, 8 Steps, blur enabled, no GI\n")); } ImGui::TableNextColumn(); @@ -581,7 +587,7 @@ void ScreenSpaceGI::SetupResources() void ScreenSpaceGI::ClearShaderCache() { static const std::vector*> shaderPtrs = { - &prefilterDepthsCompute, &prefilterRadianceCompute, &prefilterNormalCompute, &radianceDisoccCompute, &giCompute, &blurCompute, &upsampleCompute + &prefilterDepthsCompute, &prefilterRadianceCompute, &prefilterNormalCompute, &radianceDisoccCompute, &giCompute, &blurCompute, &stereoSyncCompute, &upsampleCompute }; for (auto shader : shaderPtrs) @@ -647,7 +653,7 @@ void ScreenSpaceGI::UpdateSB() float2 dynres = Util::ConvertToDynamic(res); dynres = { floor(dynres.x), floor(dynres.y) }; - static float4x4 prevInvView = {}; + static float4x4 prevInvView[2] = {}; SSGICB data; { @@ -660,7 +666,7 @@ void ScreenSpaceGI::UpdateSB() if (globals::game::isVR) data.NDCToViewMul[eyeIndex].x *= 2; - prevInvView = eye.viewMat.Invert(); + prevInvView[eyeIndex] = eye.viewMat.Invert(); } data.TexDim = res; @@ -702,7 +708,7 @@ void ScreenSpaceGI::DrawSSGI() auto context = globals::d3d::context; auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - auto& BSImagespaceShaderISSAOBlurH = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISSAOBlurH; + GET_INSTANCE_MEMBER(BSImagespaceShaderISSAOBlurH, imageSpaceManager); // Toggle vanilla SSAO static bool* enableSSAO = reinterpret_cast(reinterpret_cast(BSImagespaceShaderISSAOBlurH.get()) + 0x50LL); @@ -738,7 +744,7 @@ void ScreenSpaceGI::DrawSSGI() auto rts = renderer->GetRuntimeData().renderTargets; auto deferred = globals::deferred; - float2 size = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); + float2 size = Util::ConvertToDynamic(globals::state->screenSize); auto resolution = std::array{ (uint)size.x, (uint)size.y }; auto resChoices = std::array{ resolution, std::array{ resolution[0] >> 1, resolution[1] >> 1 }, std::array{ resolution[0] >> 2, resolution[1] >> 2 } diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index bd566a93dd..133eb43fa7 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -8,6 +8,7 @@ struct ScreenSpaceGI : Feature static constexpr std::string_view MOD_ID = "130375"; public: + bool inline SupportsVR() override { return true; } virtual inline std::string GetName() override { return "Screen Space GI"; } virtual std::string GetDisplayName() override { return T("feature.screen_space_gi.name", "Screen Space GI"); } @@ -65,8 +66,8 @@ struct ScreenSpaceGI : Feature bool EnableExperimentalSpecularGI = false; bool EnableVanillaSSAO = false; // performance/quality - uint NumSlices = 4u; - uint NumSteps = 8u; + uint NumSlices = REL::Module::IsVR() ? 3u : 4u; // AO preset for VR + uint NumSteps = REL::Module::IsVR() ? 6u : 8u; int ResolutionMode = 1; // 0-full, 1-half, 2-quarter - DBF default // visual float MinScreenRadius = 0.01f; @@ -92,9 +93,9 @@ struct ScreenSpaceGI : Feature struct alignas(16) SSGICB { - float4x4 PrevInvViewMat; - float4 NDCToViewMul; - float4 NDCToViewAdd; + float4x4 PrevInvViewMat[2]; + float2 NDCToViewMul[2]; + float2 NDCToViewAdd[2]; float2 TexDim; float2 RcpTexDim; // @@ -168,5 +169,6 @@ struct ScreenSpaceGI : Feature winrt::com_ptr radianceDisoccCompute = nullptr; winrt::com_ptr giCompute = nullptr; winrt::com_ptr blurCompute = nullptr; + winrt::com_ptr stereoSyncCompute = nullptr; winrt::com_ptr upsampleCompute = nullptr; }; diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index cf3b6621ca..c5452d91fe 100644 --- a/src/Features/ScreenSpaceShadows.cpp +++ b/src/Features/ScreenSpaceShadows.cpp @@ -45,6 +45,15 @@ void ScreenSpaceShadows::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("%s", T(TKEY("shadow_contrast_tooltip"), "Contrast boost for the shadow transition. Higher values produce harder shadow edges.")); + if (globals::game::isVR && globals::state->IsDeveloperMode()) { + ImGui::Checkbox(T(TKEY("vr_stereo_sync"), "VR Stereo Sync"), &enableStereoSync); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("%s", T(TKEY("vr_stereo_sync_tooltip"), + "Synchronizes shadow data between left and right eyes via bilateral reprojection " + "and applies a depth-weighted blur to reduce per-eye noise. " + "Uses min-blend so if either eye detects an occluder, the shadow is preserved. ")); + } + ImGui::Spacing(); ImGui::Spacing(); ImGui::TreePop(); @@ -57,16 +66,31 @@ void ScreenSpaceShadows::InvalidateRaymarchShaders() raymarchCS->Release(); raymarchCS = nullptr; } + if (raymarchRightCS) { + raymarchRightCS->Release(); + raymarchRightCS = nullptr; + } } void ScreenSpaceShadows::ClearShaderCache() { InvalidateRaymarchShaders(); + if (stereoSyncCS) { + stereoSyncCS->Release(); + stereoSyncCS = nullptr; + } } uint ScreenSpaceShadows::GetScaledSampleCount() { - float2 renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); + if (globals::game::isVR) { + // In VR, SAMPLE_COUNT is a pixel-space ray length that is FOV-driven, not resolution-driven. + // Resolution-scaling produced 2-8x excess samples at VR resolutions with no quality benefit. + // WAVE_SIZE (64) alignment is required for correct Bend READ_COUNT computation. + return bendSettings.SampleCount * 64; + } + + float2 renderSize = Util::ConvertToDynamic(globals::state->screenSize); // Scale sample count based on both dimensions relative to 1920x1080 reference float2 referenceRes = { 1920.0f, 1080.0f }; @@ -103,10 +127,24 @@ ID3D11ComputeShader* ScreenSpaceShadows::GetComputeRaymarch() return raymarchCS; } +ID3D11ComputeShader* ScreenSpaceShadows::GetComputeRaymarchRight() +{ + if (!raymarchRightCS) { + uint scaledSampleCount = GetScaledSampleCount(); + auto sampleCount = std::format("{}", scaledSampleCount); + std::vector> defines{ { "SAMPLE_COUNT", sampleCount.c_str() }, { "RIGHT", "" } }; + if (globals::features::terrainBlending.loaded) + defines.push_back({ "TERRAIN_BLENDING", "" }); + raymarchRightCS = (ID3D11ComputeShader*)Util::CompileShader(L"Data\\Shaders\\ScreenSpaceShadows\\RaymarchCS.hlsl", defines, "cs_5_0"); + } + return raymarchRightCS; +} + void ScreenSpaceShadows::DrawShadows() { ZoneScopedS(8); - TracyD3D11Zone(globals::state->tracyCtx, "Screen Space Shadows"); + auto state = globals::state; + TracyD3D11Zone(state->tracyCtx, "Screen Space Shadows"); auto context = globals::d3d::context; @@ -118,18 +156,21 @@ void ScreenSpaceShadows::DrawShadows() light.Normalize(); float4 lightProjection = float4(-light.x, -light.y, -light.z, 0.0f); - // Helper lambda to calculate light projection - auto CalculateLightProjection = [&]() -> std::array { - auto viewProjMat = globals::game::frameBufferCached.GetCameraViewProj().Transpose(); + // Helper lambda to calculate light projection for a given eye + auto CalculateLightProjection = [&](uint32_t eyeIndex = 0) -> std::array { + auto viewProjMat = globals::game::frameBufferCached.GetCameraViewProj(eyeIndex).Transpose(); auto projectedLight = DirectX::SimpleMath::Vector4::Transform(lightProjection, viewProjMat); return { projectedLight.x, projectedLight.y, projectedLight.z, projectedLight.w }; }; - auto lightProjectionF = CalculateLightProjection(); + auto lightProjectionF = CalculateLightProjection(0); - float2 renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); + float2 renderSize = Util::ConvertToDynamic(state->screenSize); int viewportSize[2] = { (int)renderSize.x, (int)renderSize.y }; + if (globals::game::isVR) + viewportSize[0] /= 2; + int minRenderBounds[2] = { 0, 0 }; int maxRenderBounds[2] = { viewportSize[0], viewportSize[1] }; @@ -154,12 +195,16 @@ void ScreenSpaceShadows::DrawShadows() float2 dynamicRes = { viewport->GetRuntimeData().dynamicResolutionWidthRatio, viewport->GetRuntimeData().dynamicResolutionHeightRatio }; - // Shared dispatch logic - auto Dispatch = [&](ID3D11ComputeShader* shader, const float* lightProj, - float invTexSizeX, float invTexSizeY) { - globals::profiler->BeginPass("ScreenSpaceShadows::RayMarch"); + // Shared dispatch logic for both VR and non-VR + auto DispatchEye = [&](const char* eyeName, ID3D11ComputeShader* shader, const float* lightProj, + float invTexSizeX, float invTexSizeY) { + std::string timerName = eyeName ? std::format("ScreenSpaceShadows::RayMarch({})", eyeName) : "ScreenSpaceShadows::RayMarch"; + globals::profiler->BeginPass(timerName); - if (globals::state->frameAnnotations) { + if (globals::state->frameAnnotations && eyeName) { + std::string eventName = std::format("SSS - Ray March ({})", eyeName); + globals::state->BeginPerfEvent(eventName); + } else if (globals::state->frameAnnotations) { globals::state->BeginPerfEvent("SSS - Ray March"); } @@ -171,7 +216,7 @@ void ScreenSpaceShadows::DrawShadows() auto dispatchData = dispatchList.Dispatch[i]; { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - Dispatch CB"); + TracyD3D11Zone(globals::state->tracyCtx, "SSS - DispatchEye CB"); RaymarchCB data{}; data.LightCoordinate[0] = dispatchList.LightCoordinate_Shader[0]; @@ -196,7 +241,7 @@ void ScreenSpaceShadows::DrawShadows() } { - TracyD3D11Zone(globals::state->tracyCtx, "SSS - Dispatch Sweep"); + TracyD3D11Zone(globals::state->tracyCtx, "SSS - DispatchEye Sweep"); context->Dispatch(dispatchData.WaveCount[0], dispatchData.WaveCount[1], dispatchData.WaveCount[2]); } } @@ -211,7 +256,21 @@ void ScreenSpaceShadows::DrawShadows() float InvTexSizeX = 1.0f / (float)viewportSize[0]; float InvTexSizeY = 1.0f / (float)viewportSize[1]; - Dispatch(GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); + if (!globals::game::isVR) { + DispatchEye(nullptr, GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); + } else { + { + TracyD3D11Zone(globals::state->tracyCtx, "SSS - Left Eye"); + DispatchEye("Left Eye", GetComputeRaymarch(), lightProjectionF.data(), InvTexSizeX, InvTexSizeY); + } + + // Calculate light projection for right eye + auto lightProjectionRightF = CalculateLightProjection(1); + { + TracyD3D11Zone(globals::state->tracyCtx, "SSS - Right Eye"); + DispatchEye("Right Eye", GetComputeRaymarchRight(), lightProjectionRightF.data(), InvTexSizeX, InvTexSizeY); + } + } ID3D11ShaderResourceView* views[1]{ nullptr }; context->CSSetShaderResources(0, 1, views); @@ -228,6 +287,73 @@ void ScreenSpaceShadows::DrawShadows() context->CSSetConstantBuffers(1, 1, &buffer); } +void ScreenSpaceShadows::DrawStereoSync() +{ + if (!globals::game::isVR || !enableStereoSync || !stereoSyncCopyTex || !stereoSyncCB) + return; + + if (!stereoSyncCS) { + std::vector> defines{ { "VR", "" }, { "FRAMEBUFFER", "" } }; + if (globals::features::terrainBlending.loaded) + defines.push_back({ "TERRAIN_BLENDING", "" }); + stereoSyncCS = reinterpret_cast(Util::CompileShader(L"Data\\Shaders\\ScreenSpaceShadows\\StereoSyncCS.hlsl", defines, "cs_5_0")); + } + if (!stereoSyncCS) + return; + + ZoneScoped; + TracyD3D11Zone(globals::state->tracyCtx, "SSS - Stereo Sync"); + + if (globals::state->frameAnnotations) + globals::state->BeginPerfEvent("SSS - Stereo Sync"); + + auto context = globals::d3d::context; + globals::profiler->BeginPass("ScreenSpaceShadows::StereoSync"); + + context->CopyResource(stereoSyncCopyTex->resource.get(), screenSpaceShadowsTexture->resource.get()); + + float2 resolution = Util::ConvertToDynamic(globals::state->screenSize); + + StereoSyncCB cbData{}; + cbData.FrameDim[0] = resolution.x; + cbData.FrameDim[1] = resolution.y; + cbData.RcpFrameDim[0] = 1.0f / resolution.x; + cbData.RcpFrameDim[1] = 1.0f / resolution.y; + + stereoSyncCB->Update(cbData); + auto cbPtr = stereoSyncCB->CB(); + + // Same 24/32-bit depth path as the raymarch — SrcDepthTexture's HLSL type is + // conditional on TERRAIN_BLENDING via the define passed at compile time below. + auto* depthSRV = Util::GetCurrentSceneDepthSRV(false); + ID3D11ShaderResourceView* srvs[2]{ depthSRV, stereoSyncCopyTex->srv.get() }; + ID3D11UnorderedAccessView* uavs[1]{ screenSpaceShadowsTexture->uav.get() }; + + context->CSSetConstantBuffers(1, 1, &cbPtr); + auto* sharedDataBuf = globals::state->sharedDataCB->CB(); + context->CSSetConstantBuffers(5, 1, &sharedDataBuf); + context->CSSetShaderResources(0, 2, srvs); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetShader(stereoSyncCS, nullptr, 0); + + auto dispatchCount = Util::GetScreenDispatchCount(true); + context->Dispatch(dispatchCount.x, dispatchCount.y, 1); + + srvs[0] = nullptr; + srvs[1] = nullptr; + uavs[0] = nullptr; + cbPtr = nullptr; + context->CSSetShaderResources(0, 2, srvs); + context->CSSetUnorderedAccessViews(0, 1, uavs, nullptr); + context->CSSetConstantBuffers(1, 1, &cbPtr); + context->CSSetShader(nullptr, nullptr, 0); + + globals::profiler->EndPass(); + + if (globals::state->frameAnnotations) + globals::state->EndPerfEvent(); +} + void ScreenSpaceShadows::Prepass() { auto context = globals::d3d::context; @@ -238,6 +364,7 @@ void ScreenSpaceShadows::Prepass() if (auto sky = globals::game::sky) if (bendSettings.Enable && sky->mode.get() == RE::Sky::Mode::kFull) { DrawShadows(); + DrawStereoSync(); } auto view = screenSpaceShadowsTexture->srv.get(); @@ -268,6 +395,10 @@ void ScreenSpaceShadows::SetupResources() { raymarchCB = new ConstantBuffer(ConstantBufferDesc(), "SSS::RaymarchCB"); + if (globals::game::isVR) { + stereoSyncCB = new ConstantBuffer(ConstantBufferDesc(), "SSS::StereoSyncCB"); + } + { auto device = globals::d3d::device; @@ -310,6 +441,11 @@ void ScreenSpaceShadows::SetupResources() screenSpaceShadowsTexture = new Texture2D(texDesc, "SSS::ShadowTexture"); screenSpaceShadowsTexture->CreateSRV(srvDesc); screenSpaceShadowsTexture->CreateUAV(uavDesc); + + if (globals::game::isVR) { + stereoSyncCopyTex = new Texture2D(texDesc, "SSS::StereoSyncCopy"); + stereoSyncCopyTex->CreateSRV(srvDesc); + } } } #undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenSpaceShadows.h b/src/Features/ScreenSpaceShadows.h index fadd8658dc..48c79556c1 100644 --- a/src/Features/ScreenSpaceShadows.h +++ b/src/Features/ScreenSpaceShadows.h @@ -25,9 +25,9 @@ struct ScreenSpaceShadows : Feature struct BendSettings { - float SurfaceThickness = 0.02f; + float SurfaceThickness = !globals::game::isVR ? 0.02f : 0.010f; float BilinearThreshold = 0.02f; - float ShadowContrast = 1.0f; + float ShadowContrast = !globals::game::isVR ? 1.0f : 4.0f; uint Enable = 1; uint SampleCount = 1; uint pad0[3]; @@ -56,13 +56,28 @@ struct ScreenSpaceShadows : Feature }; STATIC_ASSERT_ALIGNAS_16(RaymarchCB); + bool enableStereoSync = true; + + struct alignas(16) StereoSyncCB + { + float FrameDim[2]; + float RcpFrameDim[2]; + }; + STATIC_ASSERT_ALIGNAS_16(StereoSyncCB); + ID3D11SamplerState* pointBorderSampler = nullptr; ConstantBuffer* raymarchCB = nullptr; ID3D11ComputeShader* raymarchCS = nullptr; + ID3D11ComputeShader* raymarchRightCS = nullptr; Texture2D* screenSpaceShadowsTexture = nullptr; + // VR stereo sync resources + Texture2D* stereoSyncCopyTex = nullptr; + ConstantBuffer* stereoSyncCB = nullptr; + ID3D11ComputeShader* stereoSyncCS = nullptr; + virtual void SetupResources() override; virtual void DrawSettings() override; @@ -72,6 +87,7 @@ struct ScreenSpaceShadows : Feature uint GetScaledSampleCount(); uint lastCompiledSampleCount = 0; ID3D11ComputeShader* GetComputeRaymarch(); + ID3D11ComputeShader* GetComputeRaymarchRight(); virtual void Prepass() override; @@ -79,7 +95,9 @@ struct ScreenSpaceShadows : Feature virtual void SaveSettings(json& o_json) override; void DrawShadows(); + void DrawStereoSync(); virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index 67381d2d89..c5a87cb8f7 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -18,6 +18,7 @@ struct ScreenshotFeature : public Feature virtual std::string GetShortName() override { return "Screenshot"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kUtility; } + virtual bool SupportsVR() override { return true; } virtual bool IsInMenu() const override; virtual void DrawSettings() override; @@ -35,7 +36,7 @@ struct ScreenshotFeature : public Feature std::string screenshotPath = "Screenshots"; // HDR PNG quantization (7-16); used when HDR Display captures the back buffer. unsigned int hdrPngBitDepth = 11; - // SDR output (HDR captures always use PNG). + // SDR / VR output (HDR captures always use PNG). bool sdrUsePng = false; // After save, put the file path on the clipboard (CF_HDROP). bool copyToClipboard = false; diff --git a/src/Features/Skin.h b/src/Features/Skin.h index b5af9053d6..9e21044810 100644 --- a/src/Features/Skin.h +++ b/src/Features/Skin.h @@ -30,6 +30,7 @@ struct Skin : Feature return t == RE::BSShader::Type::Lighting; }; + virtual inline bool SupportsVR() { return true; } virtual void RestoreDefaultSettings() override; virtual void DrawSettings() override; diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 779fb31988..2290fe1819 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -50,6 +50,7 @@ struct SkySync : Feature virtual void RestoreDefaultSettings() override; virtual bool IsCore() const override { return true; } + virtual bool SupportsVR() override { return true; } void OnSkyUpdateColors(RE::Sky* sky); diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index dae0af6cb8..91bdbdbc42 100644 --- a/src/Features/Skylighting.cpp +++ b/src/Features/Skylighting.cpp @@ -174,7 +174,7 @@ Skylighting::SkylightingCB Skylighting::GetCommonBufferData(bool a_inWorld) static float3 prevCellID = { 0, 0, 0 }; - auto eyePosNI = Util::GetEyePosition(); + auto eyePosNI = Util::GetEyePosition(0); auto eyePos = float3{ eyePosNI.x, eyePosNI.y, eyePosNI.z }; float3 cellSize = { @@ -259,7 +259,7 @@ void Skylighting::PostPostLoad() { logger::info("[SKYLIGHTING] Hooking BSLightingShaderProperty::GetPrecipitationOcclusionMapRenderPassesImp"); stl::write_vfunc<0x2D, BSLightingShaderProperty_GetPrecipitationOcclusionMapRenderPassesImpl>(RE::VTABLE_BSLightingShaderProperty[0]); - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); if (globals::game::isVR) stl::write_thunk_call(REL::RelocationID(25643, 26185).address() + REL::Relocate(0x5D9, 0x59D, 0x5DC)); @@ -461,6 +461,24 @@ void Skylighting::SetViewFrustum::thunk(RE::NiCamera* a_camera, RE::NiFrustum* a func(a_camera, a_frustum); } +void Skylighting::SetViewFrustumVR::thunk(RE::NiCamera* a_camera, RE::NiFrustum* a_frustum, uint a_eyeIndex) +{ + auto& skylighting = globals::features::skylighting; + + if (skylighting.inOcclusion) { + uint corner = skylighting.frameCount % 4; + + float frustumSize = a_frustum->fTop; + + a_frustum->fBottom = (corner == 0 || corner == 1) ? -frustumSize : 0.0f; + a_frustum->fLeft = (corner == 0 || corner == 2) ? -frustumSize : 0.0f; + a_frustum->fRight = (corner == 1 || corner == 3) ? frustumSize : 0.0f; + a_frustum->fTop = (corner == 2 || corner == 3) ? frustumSize : 0.0f; + } + + func(a_camera, a_frustum, a_eyeIndex); +} + void Skylighting::RenderOcclusion() { ZoneScopedS(8); diff --git a/src/Features/Skylighting.h b/src/Features/Skylighting.h index 78aa3ee0c6..900f709ab2 100644 --- a/src/Features/Skylighting.h +++ b/src/Features/Skylighting.h @@ -6,6 +6,7 @@ struct Skylighting : Feature static constexpr std::string_view MOD_ID = "139352"; public: + virtual bool SupportsVR() override { return true; }; virtual inline std::string GetName() override { return "Skylighting"; } virtual std::string GetDisplayName() override { return T("feature.skylighting.name", "Skylighting"); } @@ -113,6 +114,12 @@ struct Skylighting : Feature static inline REL::Relocation func; }; + struct SetViewFrustumVR + { + static void thunk(RE::NiCamera* a_camera, RE::NiFrustum* a_frustum, uint a_eyeIndex); + static inline REL::Relocation func; + }; + // Event handler class MenuOpenCloseEventHandler : public RE::BSTEventSink { diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index 6e4136d425..591d1ae194 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -230,7 +230,7 @@ void SubsurfaceScattering::DrawSSS() auto dispatchCount = Util::GetScreenDispatchCount(); { - auto cameraData = globals::game::shadowState->GetRuntimeData().cameraData.getEye(); + auto cameraData = Util::GetCameraData(0); blurCBData.SSSS_FOVY = atan(1.0f / cameraData.projMat.m[0][0]) * 2.0f * (180.0f / 3.14159265359f); diff --git a/src/Features/SubsurfaceScattering.h b/src/Features/SubsurfaceScattering.h index edcc3954d2..7ebd3ae33e 100644 --- a/src/Features/SubsurfaceScattering.h +++ b/src/Features/SubsurfaceScattering.h @@ -135,4 +135,5 @@ struct SubsurfaceScattering : Feature } }; + virtual bool SupportsVR() override { return true; }; }; diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index b5e467eeac..b298fdbb13 100644 --- a/src/Features/TerrainBlending.cpp +++ b/src/Features/TerrainBlending.cpp @@ -6,6 +6,7 @@ #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" +#include "VR.h" #define I18N_KEY_PREFIX "feature.terrain_blending." @@ -43,7 +44,7 @@ namespace // 1) PS slot 17 override: bind TB-selected depth SRV for OBB depth reads; prevents occlusion instability / mesh popping. // 2) PS slot 2 override: bind TB-selected depth SRV for shadowmask reads; prevents unstable/moving ground shadow imprint, and dark overlay style artifacts. // 3) OM depth override: force DepthFunc=ALWAYS only on descriptor 0x1062002; mitigate shadowmask ground artifacts caused by failed depth testing in 0x1062002. - // All override paths below are gated by IsEngineHookFeatureGateSatisfied. + // All override paths below are gated by IsEngineHookFeatureGateSatisfied and all are VR-specific at runtime (isVR, gateSatisfied). // Developer Mode only: logs one hook snapshot per session ([TB Override]/[TB DepthOverride]) and explicit fallback activate/reset events. // Fallbacks: caller fallback is in ShouldAllowCallerWithFallback(...) (2 and 3 widen after 5 rejects and collapse on first allowlisted hit), SRV-source fallback is in Util::GetCurrentSceneDepthSRV(...). // Pixel descriptors: @@ -52,9 +53,27 @@ namespace constexpr uint32_t kShadowmaskDepthDescriptor0 = 0x262002u; constexpr uint32_t kShadowmaskDepthDescriptor1 = 0x1062002u; - // Allowlists of module RVAs for _ReturnAddress()-based hook dispatch. - const std::array kSlot2CallerAllowlistRvas = {}; - const std::array kDepthOverrideCallerAllowlistRvas = {}; + // Module RVAs from _ReturnAddress() at hooked engine callsites. + // Ownership: + // - Shared slot2 + depth-override callers: 0x1351AD4, 0xDBDD68 + // - Depth-override-only caller: 0x1349B7F + const uint32_t kCallerRvaSlot2AndDepthOverrideA = static_cast(REL::Relocate(0u, 0u, 0x1351AD4u)); + const uint32_t kCallerRvaSlot2AndDepthOverrideB = static_cast(REL::Relocate(0u, 0u, 0xDBDD68u)); + const uint32_t kCallerRvaDepthOverrideOnly = static_cast(REL::Relocate(0u, 0u, 0x1349B7Fu)); + + // Slot2 rewrite allowlist (PS slot 2 = shadowmask depth SRV override path). + // Includes only callsites validated for shadowmask slot2 rebinding. + const std::array kSlot2CallerAllowlistRvas = { + kCallerRvaSlot2AndDepthOverrideA, + kCallerRvaSlot2AndDepthOverrideB + }; + // Descriptor-scoped OM depth override allowlist (0x1062002 only). + // Contains the two shared callers above plus one depth-override-only caller. + const std::array kDepthOverrideCallerAllowlistRvas = { + kCallerRvaSlot2AndDepthOverrideB, + kCallerRvaSlot2AndDepthOverrideA, + kCallerRvaDepthOverrideOnly + }; constexpr bool kEnableAutoBroadSlot2Fallback = true; constexpr uint64_t kSlot2AutoFallbackRejectThreshold = 5; constexpr bool kEnableAutoBroadDepthOverrideFallback = true; @@ -99,7 +118,8 @@ namespace bool ShouldUseBlendedDepthSRV() { - return true; + auto& vr = globals::features::vr; + return !globals::game::isVR || !vr.gDepthBufferCulling || !*vr.gDepthBufferCulling; } bool IsShadowmaskDepthDescriptorWhitelisted(const uint32_t a_descriptor) @@ -208,9 +228,13 @@ namespace a_callerRva); } - bool IsEngineHookFeatureGateSatisfied([[maybe_unused]] const TerrainBlending& a_singleton) + bool IsEngineHookFeatureGateSatisfied(const TerrainBlending& a_singleton) { - return false; + if (!globals::game::isVR || !a_singleton.loaded || !a_singleton.settings.Enabled) { + return false; + } + + return !ShouldUseBlendedDepthSRV(); } struct EngineHookPassGateState @@ -727,7 +751,7 @@ void TerrainBlending::TerrainShaderHacks() auto dsv = terrainDepth.views[0]; context->OMSetRenderTargets(0, nullptr, dsv); auto shadowState = globals::game::shadowState; - auto& currentVertexShader = shadowState->GetRuntimeData().currentVertexShader; + GET_INSTANCE_MEMBER(currentVertexShader, shadowState) context->VSSetShader((ID3D11VertexShader*)currentVertexShader->shader, NULL, NULL); } renderAltTerrain = !renderAltTerrain; @@ -802,7 +826,7 @@ void TerrainBlending::BlendPrepassDepths() auto stateUpdateFlags = globals::game::stateUpdateFlags; stateUpdateFlags->set(RE::BSGraphics::ShaderFlags::DIRTY_RENDERTARGET); // CopyResource(terrainDepth <- mainDepth) eliminated: main depth is now written - // directly into mainDepthCopy (u2) by the CS above, saving a full D24S8 copy. + // directly into mainDepthCopy (u2) by the CS above, saving a full-stereo D24S8 copy. if (globals::state->frameAnnotations) globals::state->EndPerfEvent(); @@ -837,7 +861,7 @@ void TerrainBlending::Hooks::Main_RenderDepth::thunk(bool a1, bool a2) globals::game::graphicsState->SetCameraData(RE::Main::WorldRootCamera(), 1); - singleton.eyePosition = Util::GetEyePosition(); + singleton.averageEyePosition = Util::GetAverageEyePosition(); const bool tbActive = shaderCache->IsEnabled() && singleton.settings.Enabled; const bool useBlendedDepthSRV = tbActive && ShouldUseBlendedDepthSRV(); @@ -889,7 +913,7 @@ void TerrainBlending::Hooks::BSBatchRenderer__RenderPassImmediately::thunk(RE::B bool inTerrain = a_pass->shaderProperty && a_pass->shaderProperty->flags.all(RE::BSShaderProperty::EShaderPropertyFlag::kMultiTextureLandscape); if (inTerrain && a_pass->geometry) { - if ((a_pass->geometry->worldBound.center.GetDistance(singleton.eyePosition) - a_pass->geometry->worldBound.radius) > 1024.0f) { + if ((a_pass->geometry->worldBound.center.GetDistance(singleton.averageEyePosition) - a_pass->geometry->worldBound.radius) > 1024.0f) { inTerrain = false; } } @@ -1001,9 +1025,9 @@ void TerrainBlending::RenderTerrainBlendingPasses() if (globals::state->frameAnnotations) globals::state->BeginPerfEvent("Terrain Blending - Render Passes"); - auto& alphaBlendMode = shadowState->GetRuntimeData().alphaBlendMode; - auto& alphaBlendWriteMode = shadowState->GetRuntimeData().alphaBlendWriteMode; - auto& depthStencilDepthMode = shadowState->GetRuntimeData().depthStencilDepthMode; + GET_INSTANCE_MEMBER(alphaBlendMode, shadowState) + GET_INSTANCE_MEMBER(alphaBlendWriteMode, shadowState) + GET_INSTANCE_MEMBER(depthStencilDepthMode, shadowState) // Reset alpha write and enable alpha blending alphaBlendWriteMode = 1; diff --git a/src/Features/TerrainBlending.h b/src/Features/TerrainBlending.h index 04f8c6bb06..8f13612eb9 100644 --- a/src/Features/TerrainBlending.h +++ b/src/Features/TerrainBlending.h @@ -23,6 +23,7 @@ struct TerrainBlending : Feature }; virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; } + virtual bool SupportsVR() override { return true; } struct Settings { @@ -54,7 +55,7 @@ struct TerrainBlending : Feature bool renderTerrainDepth = false; bool renderAltTerrain = false; - RE::NiPoint3 eyePosition; + RE::NiPoint3 averageEyePosition; struct RenderPass { @@ -149,7 +150,7 @@ struct TerrainBlending : Feature static void Install() { // To know when we are rendering z-prepass depth vs shadows depth - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x395, 0x395)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x395, 0x395, 0x2EE)); // To know when shadowmask phase ends (for releasing engine hook overrides) stl::detour_thunk(REL::RelocationID(100422, 107140)); diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index 6bef135c62..bcd09054e9 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -228,7 +228,8 @@ void TerrainHelper::Load() { // Install TESObjectLAND hook early so TH is inner relative to TruePBR's PostPostLoad hook. // This ensures TH reads the vanilla material hashKey before TruePBR replaces it with a PBR material. - // This intentionally matches TruePBR's REL::RelocationID(18368, 18791). + // This intentionally matches TruePBR's REL::RelocationID(18368, 18791), so no extra + // VR gate is needed unless those offsets diverge. logger::info("[Terrain Helper] Hooking TESObjectLAND"); stl::detour_thunk(REL::RelocationID(18368, 18791)); } diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index 796c1628da..c477a27ce0 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -39,6 +39,7 @@ struct TerrainHelper : Feature virtual void Load() override; virtual void DataLoaded() override; virtual void PostPostLoad() override; + virtual bool SupportsVR() override { return true; }; virtual std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } void SetShaderResources(ID3D11DeviceContext* a_context); diff --git a/src/Features/TerrainShadows.h b/src/Features/TerrainShadows.h index dd79a56f23..3d824de82c 100644 --- a/src/Features/TerrainShadows.h +++ b/src/Features/TerrainShadows.h @@ -91,5 +91,6 @@ struct TerrainShadows : public Feature virtual inline void RestoreDefaultSettings() override { settings = {}; } virtual void ClearShaderCache() override; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; \ No newline at end of file diff --git a/src/Features/TerrainVariation.h b/src/Features/TerrainVariation.h index 66dc538276..83bf3385f1 100644 --- a/src/Features/TerrainVariation.h +++ b/src/Features/TerrainVariation.h @@ -16,6 +16,7 @@ struct TerrainVariation : Feature return (shaderType == RE::BSShader::Type::Lighting); } virtual bool IsCore() const override { return false; }; + virtual bool SupportsVR() override { return true; } virtual std::string_view GetCategory() const override { return FeatureCategories::kLandscapeAndTextures; } virtual std::pair> GetFeatureSummary() override diff --git a/src/Features/UnifiedWater.cpp b/src/Features/UnifiedWater.cpp index 9e719a4cd3..d3121345be 100644 --- a/src/Features/UnifiedWater.cpp +++ b/src/Features/UnifiedWater.cpp @@ -350,7 +350,7 @@ void UnifiedWater::PostPostLoad() stl::detour_thunk(REL::RelocationID(13170, 13315)); stl::detour_thunk(REL::RelocationID(20029, 20463)); - stl::write_thunk_call(REL::RelocationID(31388, 32179).address() + REL::Relocate(0x360, 0x3BC)); + stl::write_thunk_call(REL::RelocationID(31388, 32179).address() + REL::Relocate(0x360, 0x3BC, 0x35B)); stl::write_vfunc<0x4, BSWaterShaderMaterial_ComputeCRC32>(RE::VTABLE_BSWaterShaderMaterial[0]); stl::detour_thunk(REL::RelocationID(30934, 31737)); diff --git a/src/Features/UnifiedWater.h b/src/Features/UnifiedWater.h index 9ae1f05259..0de2594480 100644 --- a/src/Features/UnifiedWater.h +++ b/src/Features/UnifiedWater.h @@ -106,6 +106,7 @@ struct UnifiedWater : OverlayFeature virtual bool IsCore() const override { return true; } virtual bool IsDisabledByDefault() const override { return true; } + virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 0dc8329c50..8397ea114c 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -91,7 +91,7 @@ HRESULT WINAPI hk_D3D11CreateDeviceAndSwapChainUpscaling( pSwapChainDesc->BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; } - bool shouldProxy = true; + bool shouldProxy = !globals::game::isVR; if (shouldProxy) if (!pSwapChainDesc->Windowed) shouldProxy = false; @@ -465,51 +465,6 @@ void Upscaling::DrawSettings() ImGui::TreePop(); } - - if (lowRefreshRate && !settings.frameGenerationForceEnable) { - Util::Text::Warning("Warning: Requires a high refresh rate monitor or Force Enable Frame Generation"); - - onlyRequiresRestart = false; - } - - if (fidelityFXMissing) { - Util::Text::Warning("Warning: FidelityFX DLLs are not loaded"); - - onlyRequiresRestart = false; - } - - if (onlyRequiresRestart && settings.frameGenerationMode && !frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); - - if (!settings.frameGenerationMode && frameGenerationDx12PathActive) - Util::Text::Warning("Warning: Requires restart"); - - bool fgEnabled = settings.frameGenerationMode != 0; - if (ImGui::Checkbox(T(TKEY("frame_generation"), "Frame Generation"), &fgEnabled)) - settings.frameGenerationMode = fgEnabled ? 1 : 0; - - if (!frameGenerationDx12PathActive) - ImGui::BeginDisabled(); - - bool flEnabled = settings.frameLimitMode != 0; - if (ImGui::Checkbox(T(TKEY("frame_limit_vrr"), "Frame Limit (Variable Refresh Rate)"), &flEnabled)) - settings.frameLimitMode = flEnabled ? 1 : 0; - - if (!frameGenerationDx12PathActive) - ImGui::EndDisabled(); - - ImGui::TextWrapped("Allows frame generation to function on low refresh rate monitors. Detected: %.2f Hz", refreshRate); - bool fgForce = settings.frameGenerationForceEnable != 0; - if (ImGui::Checkbox(T(TKEY("force_enable_frame_generation"), "Force Enable Frame Generation"), &fgForce)) - settings.frameGenerationForceEnable = fgForce ? 1 : 0; - - ImGui::Checkbox(T(TKEY("frame_generation_in_menus"), "Frame Generation in Menus"), &settings.frameGenerationAllowInMenus); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_1"), "Keeps frame generation active while game menus are open.")); - ImGui::TextUnformatted(T(TKEY("frame_generation_in_menus_tooltip_2"), "May feel smoother, but increases menu input latency.")); - } - - ImGui::TreePop(); } if (streamline.reflexSupportedOnCurrentAdapter && ImGui::TreeNodeEx(T(TKEY("nvidia_reflex"), "NVIDIA Reflex"), ImGuiTreeNodeFlags_DefaultOpen)) { @@ -683,6 +638,68 @@ void Upscaling::DrawSettings() } } + // VR Debug visualization -- per-eye buffers and native inputs + if (globals::game::isVR) { + ImGui::Separator(); + static float debugRescale = 0.15f; + ImGui::SliderFloat(T(TKEY("view_resize"), "View Resize"), &debugRescale, 0.05f, 1.f); + + if (ImGui::TreeNode(T(TKEY("upscaling_intermediates"), "Upscaling Intermediates"))) { + if (vrIntermediateMotionVectors[0]) { + bool isDLSS = GetUpscaleMethod() == UpscaleMethod::kDLSS; + if (vrIntermediateColorIn[0] && vrIntermediateColorOut[0]) { + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[0], "Left Eye In", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorIn[1], "Right Eye In", debugRescale) + if (!isDLSS) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[0], "Left Eye Out", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateColorOut[1], "Right Eye Out", debugRescale) + } + BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[0], "Left Eye MVec", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateMotionVectors[1], "Right Eye MVec", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[0], "Left Eye Reactive", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateReactiveMask[1], "Right Eye Reactive", debugRescale) + if (vrIntermediateTransparencyMask[0]) { + BUFFER_VIEWER_NODE_TITLE(vrIntermediateTransparencyMask[0], "Left Eye Transparency", debugRescale) + BUFFER_VIEWER_NODE_TITLE(vrIntermediateTransparencyMask[1], "Right Eye Transparency", debugRescale) + } + } else { + ImGui::TextDisabled("%s", T(TKEY("vr_intermediates_not_created"), "VR intermediates not yet created (enter game world)")); + } + ImGui::TreePop(); + } + + if (ImGui::TreeNode(T(TKEY("native_inputs"), "Native Inputs"))) { + auto renderer = globals::game::renderer; + auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; + auto& mvec = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMOTION_VECTOR]; + auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + + auto DisplayRT = [&](const char* label, ID3D11Texture2D* tex, ID3D11ShaderResourceView* srv) { + if (srv && tex) { + D3D11_TEXTURE2D_DESC desc; + tex->GetDesc(&desc); + char buf[128]; + snprintf(buf, sizeof(buf), "%s (%ux%u)", label, desc.Width, desc.Height); + if (ImGui::TreeNode(buf)) { + ImGui::Image(srv, { desc.Width * debugRescale, desc.Height * debugRescale }); + ImGui::TreePop(); + } + } + }; + + DisplayRT("kMAIN (Color Input)", (ID3D11Texture2D*)main.texture, (ID3D11ShaderResourceView*)main.SRV); + DisplayRT("Motion Vectors", (ID3D11Texture2D*)mvec.texture, (ID3D11ShaderResourceView*)mvec.SRV); + DisplayRT("Depth", depth.texture, depth.depthSRV); + + if (reactiveMaskTexture) + BUFFER_VIEWER_NODE_TITLE(reactiveMaskTexture, "Reactive Mask", debugRescale) + if (transparencyCompositionMaskTexture) + BUFFER_VIEWER_NODE_TITLE(transparencyCompositionMaskTexture, "Transparency Mask", debugRescale) + + ImGui::TreePop(); + } + } + ImGui::Separator(); Util::DrawDllVersionTable("AMD FidelityFX DLLs (click to open folder)", FidelityFX::PluginDir, FidelityFX::dllVersions, "ffx_dll_versions"); Util::DrawDllVersionTable("NVIDIA Streamline DLLs (click to open folder)", Streamline::PluginDir, Streamline::dllVersions, "sl_dll_versions"); @@ -833,6 +850,31 @@ void Upscaling::DataLoaded() static auto fDRClampOffset = RE::GetINISetting("fDRClampOffset:Display"); fDRClampOffset->data.f = 0.0f; + // VR + DLSS workaround: rebuild the DLSS feature on cell/worldspace transitions to + // clear a persistent post-load GPU-time regression (see pendingDLSSReset comment). + if (globals::game::isVR) + MenuOpenCloseEventHandler::Register(); +} + +RE::BSEventNotifyControl Upscaling::MenuOpenCloseEventHandler::ProcessEvent( + const RE::MenuOpenCloseEvent* a_event, RE::BSTEventSource*) +{ + if (a_event && a_event->menuName == RE::LoadingMenu::MENU_NAME && !a_event->opening) + globals::features::upscaling.pendingDLSSReset.store(true, std::memory_order_relaxed); + return RE::BSEventNotifyControl::kContinue; +} + +bool Upscaling::MenuOpenCloseEventHandler::Register() +{ + static MenuOpenCloseEventHandler singleton; + auto ui = globals::game::ui; + if (!ui) { + logger::error("[Upscaling] UI event source not found; DLSS reset-on-load disabled"); + return false; + } + ui->GetEventSource()->AddEventSink(&singleton); + logger::info("[Upscaling] Registered MenuOpenCloseEventHandler for DLSS reset-on-load"); + return true; } void Upscaling::Load() @@ -908,22 +950,24 @@ void Upscaling::PostPostLoad() stl::detour_thunk(REL::RelocationID(79947, 82084)); // Calculates resolution and jitter - stl::write_thunk_call(REL::RelocationID(75460, 77245).address() + REL::Relocate(0xE5, isGOG ? 0x133 : 0xE2)); + stl::write_thunk_call(REL::RelocationID(75460, 77245).address() + REL::Relocate(0xE5, isGOG ? 0x133 : 0xE2, 0x104)); // Disables the original dynamic resolution system - REL::safe_write(REL::RelocationID(35556, 36555).address() + REL::Relocate(0x2D, 0x2D), REL::NOP5, sizeof(REL::NOP5)); + REL::safe_write(REL::RelocationID(35556, 36555).address() + REL::Relocate(0x2D, 0x2D, 0x25), REL::NOP5, sizeof(REL::NOP5)); // Performs upscaling in between volumetric lighting and post processing - stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7)); + stl::write_thunk_call(REL::RelocationID(100430, 107148).address() + REL::Relocate(0x1F0, 0x1E7, 0x206)); // Patches RSSetScissorRect calls to use dynamic resolution - stl::detour_thunk(REL::RelocationID(75564, 77365)); + // This is a PC-specific function hence it was missing + if (!globals::game::isVR) + stl::detour_thunk(REL::RelocationID(75564, 77365)); // Patches facegen texture generation to not use dynamic resolution stl::detour_thunk(REL::RelocationID(26455, 27041)); // Patches precipitation camera to not use dynamic resolution - stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1)); + stl::write_thunk_call(REL::RelocationID(35560, 36559).address() + REL::Relocate(0x3A1, 0x3A1, 0x2FA)); // Forces FXAA off stl::detour_thunk(REL::RelocationID(98974, 105626)); @@ -1125,6 +1169,18 @@ void Upscaling::CheckResources(UpscaleMethod a_upscalemethod) streamline.DestroyDLSSResources(); else if (previousUpscaleMode == UpscaleMethod::kFSR) fidelityFX.DestroyFSRResources(); + + if (globals::game::isVR) { + for (int i = 0; i < 2; i++) { + vrIntermediateColorIn[i].reset(); + vrIntermediateColorOut[i].reset(); + vrIntermediateLinearDepth[i].reset(); + vrIntermediateMotionVectors[i].reset(); + vrIntermediateReactiveMask[i].reset(); + vrIntermediateTransparencyMask[i].reset(); + } + vrIntermediateDepth.reset(); + } } if (a_upscalemethod == UpscaleMethod::kFSR) fidelityFX.CreateFSRResources(); @@ -1147,6 +1203,20 @@ ID3D11ComputeShader* Upscaling::GetEncodeTexturesCS() auto upscaleMethod = GetUpscaleMethod(); uint methodIndex = (uint)upscaleMethod; + // VR FSR needs a separate variant: DEPTH_OUTPUT converts the R24G8_TYPELESS game depth to + // R32_FLOAT so GetFfxResourceDescriptionDX11() returns a valid format instead of UNKNOWN. + if (globals::game::isVR && upscaleMethod == UpscaleMethod::kFSR) { + if (!encodeTexturesCSDepthOutput) { + logger::debug("Compiling EncodeTexturesCS.hlsl for VR FSR (FSR + DEPTH_OUTPUT)"); + std::vector> defines = { + { "FSR", "" }, + { "DEPTH_OUTPUT", "" } + }; + encodeTexturesCSDepthOutput.attach((ID3D11ComputeShader*)Util::CompileShader(L"Data/Shaders/Upscaling/EncodeTexturesCS.hlsl", defines, "cs_5_0")); + } + return encodeTexturesCSDepthOutput.get(); + } + if (!encodeTexturesCS[methodIndex]) { logger::debug("Compiling EncodeTexturesCS.hlsl for upscale method {}", methodIndex); @@ -1186,6 +1256,8 @@ ID3D11PixelShader* Upscaling::GetUnderwaterMaskUpscalePS() if (!underwaterMaskUpscalePS) { logger::debug("Compiling UnderwaterMaskPS.hlsl"); std::vector> defines = { { "PSHADER", "" } }; + if (globals::game::isVR) + defines.push_back({ "VR", "" }); underwaterMaskUpscalePS.attach((ID3D11PixelShader*)Util::CompileShader(L"Data/Shaders/Upscaling/UnderwaterMaskUpscalePS.hlsl", defines, "ps_5_0")); } @@ -1520,7 +1592,7 @@ void Upscaling::ConfigureTAA() auto upscaleMethod = GetUpscaleMethod(); auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - auto& BSImagespaceShaderISTemporalAA = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA; + GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); // Force enable TAA if needed BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod != UpscaleMethod::kNONE; @@ -1539,7 +1611,7 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) // Get full screen size auto state = globals::state; - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = state->screenSize; auto screenWidth = static_cast(screenSize.x); auto screenHeight = static_cast(screenSize.y); @@ -1604,7 +1676,10 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) } else { resolutionScale = { 1.0f, 1.0f }; - jitter.x = -a_viewport->projectionPosScaleX * screenWidth / 2.0f; + if (globals::game::isVR) + jitter.x = -a_viewport->projectionPosScaleX * screenWidth; + else + jitter.x = -a_viewport->projectionPosScaleX * screenWidth / 2.0f; jitter.y = a_viewport->projectionPosScaleY * screenHeight / 2.0f; } @@ -1620,7 +1695,8 @@ void Upscaling::ConfigureUpscaling(RE::BSGraphics::State* a_viewport) dynamicResolutionHeightRatio = resolutionScale.y; // Disable dynamic resolution unless the game explicitly enables it - runtimeData.dynamicResolutionLock = 1; + if (!globals::game::isVR) + runtimeData.dynamicResolutionLock = 1; } void Upscaling::SetupResources() @@ -1655,7 +1731,25 @@ void Upscaling::SetupResources() depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; // Write to all depth bits depthStencilDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; // Always pass depth test (write all depths) - depthStencilDesc.StencilEnable = false; // Disable stencil testing + if (globals::game::isVR) { + depthStencilDesc.StencilEnable = true; // Enable stencil testing + depthStencilDesc.StencilReadMask = 0xFF; // Read all stencil bits + depthStencilDesc.StencilWriteMask = 0xFF; // Write to all stencil bits + + // Configure front-facing stencil operations + depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; // Replace on stencil fail + depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; // Replace on depth fail + depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Replace on pass + depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; // Always pass stencil test + + // Configure back-facing stencil operations (same as front) + depthStencilDesc.BackFace.StencilFailOp = depthStencilDesc.FrontFace.StencilFailOp; + depthStencilDesc.BackFace.StencilDepthFailOp = depthStencilDesc.FrontFace.StencilDepthFailOp; + depthStencilDesc.BackFace.StencilPassOp = depthStencilDesc.FrontFace.StencilPassOp; + depthStencilDesc.BackFace.StencilFunc = depthStencilDesc.FrontFace.StencilFunc; + } else { + depthStencilDesc.StencilEnable = false; // Disable stencil testing + } DX::ThrowIfFailed(globals::d3d::device->CreateDepthStencilState(&depthStencilDesc, upscaleDepthStencilState.put())); @@ -1708,6 +1802,7 @@ void Upscaling::ClearShaderCache() for (int i = 0; i < 5; ++i) { encodeTexturesCS[i] = nullptr; // com_ptr automatically releases } + encodeTexturesCSDepthOutput = nullptr; depthRefractionUpscalePS = nullptr; // com_ptr automatically releases underwaterMaskUpscalePS = nullptr; // com_ptr automatically releases @@ -1730,7 +1825,7 @@ void Upscaling::CopySharedD3D12Resources() { // Set up viewport for fullscreen rendering - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = globals::state->screenSize; D3D11_VIEWPORT viewport = {}; viewport.TopLeftX = 0.0f; @@ -1909,7 +2004,7 @@ double Upscaling::GetRefreshRate(HWND a_window) bool Upscaling::IsFrameGenerationDx12PathActive() const { - return d3d12SwapChainActive; + return d3d12SwapChainActive && !globals::game::isVR; } bool Upscaling::IsFrameGenerationActive() const @@ -2083,30 +2178,44 @@ void Upscaling::Upscale() auto& normals = renderer->GetRuntimeData().renderTargets[globals::deferred->forwardRenderTargets[2]]; auto& depth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - auto renderSize = Util::ConvertToDynamic(float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }); - uint32_t renderWidth = (uint32_t)renderSize.x; - uint32_t renderHeight = (uint32_t)renderSize.y; + // VR: ensure per-eye intermediate textures exist before the dispatch writes into them + if (globals::game::isVR) + EnsureVRIntermediateTextures(); + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + uint32_t numEyes = globals::game::isVR ? 2 : 1; + uint32_t eyeRenderWidth = (uint32_t)(renderSize.x / numEyes); + uint32_t eyeRenderHeight = (uint32_t)renderSize.y; + + // Sources are the same combined stereo buffers for both VR and non-VR. + // The shader applies EyeOffsetX to sample the correct half. ID3D11ShaderResourceView* views[4] = { temporalAAMask.SRV, normals.SRV, motionVector.SRV, depth.depthSRV }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); context->CSSetShader(GetEncodeTexturesCS(), nullptr, 0); - UpscalingDataCB upscalingData; - upscalingData.trueSamplingDim = float2((float)renderWidth, (float)renderHeight); - upscalingDataCB->Update(upscalingData); - auto upscalingBuffer = upscalingDataCB->CB(); - context->CSSetConstantBuffers(0, 1, &upscalingBuffer); - - // u2 (MotionVectorOutput): DLSS only — 5x5 dilated MVec for ghosting reduction. - ID3D11UnorderedAccessView* uavs[4] = { - reactiveMaskTexture->uav.get(), - transparencyCompositionMaskTexture->uav.get(), - (upscaleMethod == UpscaleMethod::kDLSS) ? motionVectorCopyTexture->uav.get() : nullptr, - nullptr - }; - context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); + for (uint32_t i = 0; i < numEyes; ++i) { + uint32_t offsetX = i * eyeRenderWidth; + + UpscalingDataCB upscalingData; + upscalingData.trueSamplingDim = float2((float)eyeRenderWidth, (float)eyeRenderHeight); + upscalingData.eyeOffsetX = offsetX; + upscalingDataCB->Update(upscalingData); + auto upscalingBuffer = upscalingDataCB->CB(); + context->CSSetConstantBuffers(0, 1, &upscalingBuffer); + + // u2 (MotionVectorOutput): DLSS only — 5x5 dilated MVec for ghosting reduction. + // u3 (DepthOutput): VR FSR only — converts R24G8_TYPELESS to R32_FLOAT so + // GetFfxResourceDescriptionDX11() returns a valid format. DLSS depth is copied in Streamline.cpp. + ID3D11UnorderedAccessView* uavs[4] = { + globals::game::isVR ? vrIntermediateReactiveMask[i]->uav.get() : reactiveMaskTexture->uav.get(), + globals::game::isVR ? vrIntermediateTransparencyMask[i]->uav.get() : transparencyCompositionMaskTexture->uav.get(), + (upscaleMethod == UpscaleMethod::kDLSS) ? (globals::game::isVR ? vrIntermediateMotionVectors[i]->uav.get() : motionVectorCopyTexture->uav.get()) : nullptr, + (upscaleMethod == UpscaleMethod::kFSR && globals::game::isVR) ? vrIntermediateLinearDepth[i]->uav.get() : nullptr + }; + context->CSSetUnorderedAccessViews(0, ARRAYSIZE(uavs), uavs, nullptr); - context->Dispatch((renderWidth + 7) / 8, (renderHeight + 7) / 8, 1); + context->Dispatch((eyeRenderWidth + 7) / 8, (eyeRenderHeight + 7) / 8, 1); + } ID3D11ShaderResourceView* nullViews[4] = { nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(nullViews), nullViews); @@ -2242,7 +2351,7 @@ void Upscaling::UpscaleDepth() return; } - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = state->screenSize; if (screenSize.x <= 0.0f || screenSize.y <= 0.0f) { return; } @@ -2393,7 +2502,8 @@ void Upscaling::UpscaleDepth() context->OMSetDepthStencilState(nullptr, 0x00); - // t0: vanilla mask copy, t1: original depth. + // t0: vanilla mask copy, t1: original depth (for VR per-eye analytical mask). + // depthCopy still holds the original pre-upscale depth here (VR re-copy deferred). ID3D11ShaderResourceView* srvs[] = { underwaterMask.SRVCopy, depthCopy.depthSRV }; context->PSSetShaderResources(0, ARRAYSIZE(srvs), srvs); @@ -2413,6 +2523,12 @@ void Upscaling::UpscaleDepth() copyIfNonAliased(depthCopy.texture, depth.texture); } + // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. + if (globals::game::isVR) { + TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Depth VR Propagate"); + copyIfNonAliased(depthCopy.texture, depth.texture); + } + ID3D11ShaderResourceView* nullPSResources[3] = { nullptr, nullptr, nullptr }; context->PSSetShaderResources(0, ARRAYSIZE(nullPSResources), nullPSResources); @@ -2594,7 +2710,7 @@ void Upscaling::Main_PostProcessing::thunk(RE::ImageSpaceManager* a_this, uint32 } auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - auto& BSImagespaceShaderISTemporalAA = imageSpaceManager->GetRuntimeData().BSImagespaceShaderISTemporalAA; + GET_INSTANCE_MEMBER(BSImagespaceShaderISTemporalAA, imageSpaceManager); BSImagespaceShaderISTemporalAA->taaEnabled = upscaleMethod == UpscaleMethod::kTAA; diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index d1460ece19..bbc2835b7e 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -30,6 +30,7 @@ struct Upscaling : Feature virtual std::string GetDisplayName() override { return T("feature.upscaling.name", "Upscaling"); } virtual inline std::string GetShortName() override { return "Upscaling"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } + virtual inline bool SupportsVR() override { return true; } virtual inline bool IsCore() const override { return false; } virtual inline std::string_view GetCategory() const override { return FeatureCategories::kDisplay; } @@ -106,8 +107,9 @@ struct Upscaling : Feature struct UpscalingDataCB { - float2 trueSamplingDim; - float2 pad0; + float2 trueSamplingDim; // per-eye render dim in VR, full render dim otherwise + uint eyeOffsetX; // X offset into stereo source buffers; 0 for non-VR / left eye + uint pad0; }; ConstantBuffer* jitterCB = nullptr; @@ -197,7 +199,8 @@ struct Upscaling : Feature void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod); void DestroyUpscalingTextureResources(UpscaleMethod a_upscalemethod); - winrt::com_ptr encodeTexturesCS[4]; // One for each UpscaleMethod (kNONE, kTAA, kFSR, kDLSS) + winrt::com_ptr encodeTexturesCS[5]; // One for each UpscaleMethod + winrt::com_ptr encodeTexturesCSDepthOutput; // FSR + VR: converts R24G8_TYPELESS depth to R32_FLOAT ID3D11ComputeShader* GetEncodeTexturesCS(); winrt::com_ptr depthRefractionUpscalePS; @@ -213,10 +216,43 @@ struct Upscaling : Feature winrt::com_ptr upscaleBlendState; winrt::com_ptr upscaleRasterizerState; + // Shared VR HMD Mask Clearing + winrt::com_ptr vrClearHMDMaskCS; + winrt::com_ptr vrClearHMDMaskCB; + // Helper to dispatch mask clearing for a single eye region + void ClearHMDMask(ID3D11UnorderedAccessView* colorUAV, ID3D11ShaderResourceView* depthSRV, + uint32_t eyeWidth, uint32_t eyeHeight, uint32_t depthOffsetX, uint32_t colorOffsetX); + + // Shared VR Per-Eye Intermediate Buffers + // Owned here so both Streamline (DLSS) and FidelityFX (FSR) can use them. + eastl::unique_ptr vrIntermediateColorIn[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateColorOut[2]; // per-eye output resolution + eastl::unique_ptr vrIntermediateDepth; // right-eye render resolution (R24G8_TYPELESS, DLSS only) + eastl::unique_ptr vrIntermediateLinearDepth[2]; // per-eye render resolution (R32_FLOAT, for FSR) + eastl::unique_ptr vrIntermediateMotionVectors[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateReactiveMask[2]; // per-eye render resolution + eastl::unique_ptr vrIntermediateTransparencyMask[2]; // per-eye render resolution + + // Helper to create/resize per-eye buffers matching source formats + void CreateVRIntermediateTextures(uint32_t inWidth, uint32_t inHeight, uint32_t outWidth, uint32_t outHeight, + ID3D11Resource* colorSrc, ID3D11Resource* mvecSrc, ID3D11Resource* reactiveSrc, ID3D11Resource* transparencySrc); + // Helper: Create a Texture2D matching source format at a given size static eastl::unique_ptr CreateTextureFromSource(ID3D11Resource* src, uint32_t width, uint32_t height, bool copyBindFlags = false, bool createSRV = false, bool createUAV = false, const char* name = nullptr); + // Shared Pipeline Steps + + /// Ensures VR per-eye intermediate textures exist at the correct resolution. + /// Must be called before any per-eye EncodeTexturesCS dispatch or PreparePerEyeInputs. + void EnsureVRIntermediateTextures(); + + /// Splits the combined stereo color buffer into per-eye intermediates, copies raw + /// motion vectors, and clears the HMD hidden area. FSR-only. + /// Reactive/transparency masks are written by EncodeTexturesCS. + void PreparePerEyeInputs(ID3D11Resource* colorSrc); + void FinalizePerEyeOutputs(ID3D11Resource* colorDst); + void ConfigureTAA(); void ConfigureUpscaling(RE::BSGraphics::State* a_state); void Upscale(); @@ -250,7 +286,9 @@ struct Upscaling : Feature /// Set by MenuOpenCloseEventHandler when LoadingMenu closes (cell/worldspace transitions, /// initial load). Consumed at the start of Upscale() to force a one-frame DLSS feature - /// rebuild. + /// rebuild — works around a VR-only persistent ~2-3ms GPU regression after worldspace + /// loads that otherwise only clears when the user manually toggles DLSS/preset. VR+DLSS + /// only; flat has no repro and per-eye extent asymmetry doesn't apply. std::atomic pendingDLSSReset{ false }; void CopySharedD3D12Resources(); diff --git a/src/Features/Upscaling/DX12SwapChain.cpp b/src/Features/Upscaling/DX12SwapChain.cpp index ca4f3c6bea..5058a25832 100644 --- a/src/Features/Upscaling/DX12SwapChain.cpp +++ b/src/Features/Upscaling/DX12SwapChain.cpp @@ -40,13 +40,15 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC bool isVR = globals::game::isVR; bool fallbackUsed = false; - // Test R10G10B10A2 support for HDR capability + // Test R10G10B10A2 support (applies to both VR and non-VR for HDR capability) D3D12_FEATURE_DATA_FORMAT_SUPPORT formatSupport = { DXGI_FORMAT_R10G10B10A2_UNORM, D3D12_FORMAT_SUPPORT1_RENDER_TARGET, D3D12_FORMAT_SUPPORT2_NONE }; if (SUCCEEDED(d3d12Device->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &formatSupport, sizeof(formatSupport)))) { if ((formatSupport.Support1 & D3D12_FORMAT_SUPPORT1_RENDER_TARGET) == 0) { logger::warn("[DX12SwapChain] R10G10B10A2_UNORM not supported as render target, falling back to R8G8B8A8_UNORM"); negotiatedFormat = DXGI_FORMAT_R8G8B8A8_UNORM; fallbackUsed = true; + } else if (isVR) { + logger::info("[DX12SwapChain] VR detected with R10G10B10A2_UNORM support, attempting HDR"); } } else { logger::warn("[DX12SwapChain] CheckFeatureSupport failed for R10G10B10A2_UNORM, falling back to R8G8B8A8_UNORM"); @@ -54,9 +56,10 @@ void DX12SwapChain::CreateSwapChain(IDXGIAdapter* adapter, DXGI_SWAP_CHAIN_DESC fallbackUsed = true; } - logger::info("[DX12SwapChain] Swap chain format negotiation: attempted={}, negotiated={}, fallback={}", + logger::info("[DX12SwapChain] Swap chain format negotiation: attempted={}, negotiated={}, VR={}, fallback={}", static_cast(attemptedFormat), static_cast(negotiatedFormat), + isVR ? "true" : "false", fallbackUsed ? "true" : "false"); swapChainDesc = {}; diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index 46ce613f8d..10adcbab2f 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -152,7 +152,9 @@ void FidelityFX::Present(bool a_useFrameGeneration, bool a_isHDR) configParameters.flags = 0; configParameters.allowAsyncWorkloads = true; - auto renderSize = float2{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight } * upscaling.resolutionScale; + auto state = globals::state; + + auto renderSize = state->screenSize * upscaling.resolutionScale; configParameters.generationRect.left = (swapChain.swapChainDesc.Width - swapChain.swapChainDesc.Width) / 2; configParameters.generationRect.top = (swapChain.swapChainDesc.Height - swapChain.swapChainDesc.Height) / 2; @@ -241,6 +243,8 @@ void FidelityFX::Present(bool a_useFrameGeneration, bool a_isHDR) void FidelityFX::CreateFSRResources() { + auto state = globals::state; + // Prevent multiple allocations if (fsrScratchBuffer) { logger::warn("[FidelityFX] FSR resources already created, skipping allocation"); @@ -249,7 +253,7 @@ void FidelityFX::CreateFSRResources() auto fsrDevice = ffxGetDeviceDX11_Fsr31(globals::d3d::device); - uint32_t numContexts = 1; + uint32_t numContexts = globals::game::isVR ? 2 : 1; size_t scratchBufferSize = ffxGetScratchMemorySizeDX11(numContexts); fsrScratchBuffer = calloc(scratchBufferSize, 1); if (!fsrScratchBuffer) { @@ -266,7 +270,7 @@ void FidelityFX::CreateFSRResources() return; } - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); // PerfMode bridge: when the BSOpenVR size hook is live, state->screenSize is polluted @@ -283,36 +287,43 @@ void FidelityFX::CreateFSRResources() uint32_t renderWidth = (uint32_t)(globals::game::isVR ? renderSize.x / 2 : renderSize.x); uint32_t renderHeight = (uint32_t)renderSize.y; - FfxFsr3ContextDescription contextDescription; - contextDescription.maxRenderSize.width = renderWidth; - contextDescription.maxRenderSize.height = renderHeight; - contextDescription.maxUpscaleSize.width = displayWidth; - contextDescription.maxUpscaleSize.height = displayHeight; - contextDescription.displaySize.width = displayWidth; - contextDescription.displaySize.height = displayHeight; - contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE; - if (globals::features::hdrDisplay.loaded) { - contextDescription.flags |= FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; - contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R10G10B10A2_UNORM; - } else { - contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R8G8B8A8_UNORM; - } - contextDescription.backendInterfaceUpscaling = fsrInterface; - - if (ffxFsr3ContextCreate(&fsrContext[0], &contextDescription) != FFX_OK) { - logger::critical("[FidelityFX] Failed to initialize FSR3 context!"); - free(fsrScratchBuffer); - fsrScratchBuffer = nullptr; - return; + for (uint32_t i = 0; i < numContexts; ++i) { + FfxFsr3ContextDescription contextDescription; + contextDescription.maxRenderSize.width = renderWidth; + contextDescription.maxRenderSize.height = renderHeight; + contextDescription.maxUpscaleSize.width = displayWidth; + contextDescription.maxUpscaleSize.height = displayHeight; + contextDescription.displaySize.width = displayWidth; + contextDescription.displaySize.height = displayHeight; + contextDescription.flags = FFX_FSR3_ENABLE_UPSCALING_ONLY | FFX_FSR3_ENABLE_AUTO_EXPOSURE; + if (globals::features::hdrDisplay.loaded) { + contextDescription.flags |= FFX_FSR3_ENABLE_HIGH_DYNAMIC_RANGE; + contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R10G10B10A2_UNORM; + } else { + contextDescription.backBufferFormat = FFX_SURFACE_FORMAT_R8G8B8A8_UNORM; + } + contextDescription.backendInterfaceUpscaling = fsrInterface; + + if (ffxFsr3ContextCreate(&fsrContext[i], &contextDescription) != FFX_OK) { + logger::critical("[FidelityFX] Failed to initialize FSR3 context for eye {}!", i); + for (uint32_t j = 0; j < i; ++j) + ffxFsr3ContextDestroy(&fsrContext[j]); + free(fsrScratchBuffer); + fsrScratchBuffer = nullptr; + return; + } } - logger::info("[FidelityFX] Created FSR3 context (Display: {}x{}, Render: {}x{})", - displayWidth, displayHeight, renderWidth, renderHeight); + logger::info("[FidelityFX] Created {} FSR3 contexts (Display: {}x{}, Render: {}x{})", + numContexts, displayWidth, displayHeight, renderWidth, renderHeight); } void FidelityFX::DestroyFSRResources() { - if (ffxFsr3ContextDestroy(&fsrContext[0]) != FFX_OK) - logger::critical("[FidelityFX] Failed to destroy FSR3 context!"); + uint32_t numContexts = globals::game::isVR ? 2 : 1; + for (uint32_t i = 0; i < numContexts; ++i) { + if (ffxFsr3ContextDestroy(&fsrContext[i]) != FFX_OK) + logger::critical("[FidelityFX] Failed to destroy FSR3 context for eye {}!", i); + } // Free the scratch buffer to prevent memory leak if (fsrScratchBuffer) { @@ -349,7 +360,7 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r auto state = globals::state; auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); auto& upscaling = globals::features::upscaling; @@ -448,6 +459,78 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r renderSize.x); } - if (state->frameAnnotations) - state->EndPerfEvent(); + FfxFsr3DispatchUpscaleDescription dispatchParameters{}; + dispatchParameters.commandList = ffxGetCommandListDX11(context); + dispatchParameters.color = ffxGetResource(r_color, L"FSR3_Input_OutputColor"); + dispatchParameters.depth = ffxGetResource(r_depth, L"FSR3_InputDepth"); + dispatchParameters.motionVectors = ffxGetResource(r_mvec, L"FSR3_InputMotionVectors"); + dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); + dispatchParameters.upscaleOutput = ffxGetResource(r_output, L"FSR3_OutputColor"); + dispatchParameters.reactive = ffxGetResource(r_reactive, L"FSR3_InputReactiveMap"); + dispatchParameters.transparencyAndComposition = ffxGetResource(r_trans, L"FSR3_TransparencyAndCompositionMap"); + + dispatchParameters.motionVectorScale.x = mv_scale_x; + dispatchParameters.motionVectorScale.y = renderSize.y; + dispatchParameters.renderSize.width = r_width; + dispatchParameters.renderSize.height = (uint)renderSize.y; + + dispatchParameters.jitterOffset.x = -jitter.x; + dispatchParameters.jitterOffset.y = -jitter.y; + + dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; + dispatchParameters.cameraFar = *globals::game::cameraFar; + dispatchParameters.cameraNear = *globals::game::cameraNear; + dispatchParameters.enableSharpening = true; + dispatchParameters.sharpness = a_sharpness; + dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); + dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; + dispatchParameters.reset = false; + dispatchParameters.preExposure = 1.0f; + dispatchParameters.flags = 0; + + __try { + if (ffxFsr3ContextDispatchUpscale(&fsrContext[contextIndex], &dispatchParameters) != FFX_OK) + logger::critical("[FidelityFX] Failed to dispatch upscaling for eye {}!", contextIndex); + } __except (EXCEPTION_EXECUTE_HANDLER) { + if (!fsrDispatchCrashLogged) { + logger::critical("[FidelityFX] FSR3 dispatch crashed for eye {} - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture.", contextIndex); + fsrDispatchCrashLogged = true; + } + } + + if (state->frameAnnotations) + state->EndPerfEvent(); + }; + + if (globals::game::isVR) { + // Prepare per-eye inputs and clear mask + upscaling.PreparePerEyeInputs(a_upscalingTexture); + + uint32_t numViews = 2; + uint32_t eyeWidth = (uint32_t)(renderSize.x / 2); + for (uint32_t i = 0; i < numViews; ++i) { + DispatchFSR(i, + upscaling.vrIntermediateColorIn[i]->resource.get(), + upscaling.vrIntermediateLinearDepth[i]->resource.get(), + upscaling.vrIntermediateMotionVectors[i]->resource.get(), + upscaling.vrIntermediateReactiveMask[i]->resource.get(), + upscaling.vrIntermediateTransparencyMask[i]->resource.get(), + upscaling.vrIntermediateColorOut[i]->resource.get(), + eyeWidth, + renderSize.x / 2.0f); + } + + // Merge outputs back to kMAIN + upscaling.FinalizePerEyeOutputs(a_upscalingTexture); + } else { + DispatchFSR(0, + a_upscalingTexture, + depthTexture.texture, + a_motionVectors, + a_reactiveMask, + a_transparencyCompositionMask, + a_upscalingTexture, // Output to same texture + (uint)renderSize.x, + renderSize.x); + } } \ No newline at end of file diff --git a/src/Features/Upscaling/RCAS/RCAS.cpp b/src/Features/Upscaling/RCAS/RCAS.cpp index 576c14d714..46db9ef92e 100644 --- a/src/Features/Upscaling/RCAS/RCAS.cpp +++ b/src/Features/Upscaling/RCAS/RCAS.cpp @@ -48,8 +48,8 @@ void RCAS::ApplySharpen(ID3D11ShaderResourceView* inputSRV, ID3D11UnorderedAcces globals::profiler->BeginPass("Upscaling::RCAS"); state->BeginPerfEvent("RCAS Sharpening"); - uint32_t screenWidth = globals::game::graphicsState->screenWidth; - uint32_t screenHeight = globals::game::graphicsState->screenHeight; + uint32_t screenWidth = (uint32_t)state->screenSize.x; + uint32_t screenHeight = (uint32_t)state->screenSize.y; RCASConfig config{}; config.sharpness = sharpness; diff --git a/src/Features/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 545018721c..5c9f8b19e5 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -321,7 +321,7 @@ bool Streamline::EnsureFrameToken() return frameToken != nullptr; } -bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport) +bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex) { if (!globals::features::upscaling.streamline.initialized) return false; @@ -329,27 +329,50 @@ bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport) if (!EnsureFrameToken()) return false; + // In VR, we need to set constants for each viewport/eye separately + // In non-VR, this is called once per frame + auto state = globals::state; + sl::Constants slConstants = {}; - slConstants.cameraAspectRatio = (float)globals::game::graphicsState->screenWidth / (float)globals::game::graphicsState->screenHeight; + // Calculate aspect ratio for the SINGLE EYE + float eyeWidth = state->screenSize.x * (globals::game::isVR ? 0.5f : 1.0f); + slConstants.cameraAspectRatio = eyeWidth / state->screenSize.y; slConstants.cameraFOV = Util::GetVerticalFOVRad(); slConstants.cameraNear = *globals::game::cameraNear; slConstants.cameraFar = *globals::game::cameraFar; - auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse().Transpose(); - auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered().Transpose(); + auto viewMatrix = globals::game::frameBufferCached.GetCameraViewInverse(eyeIndex).Transpose(); + auto cameraViewToClip = globals::game::frameBufferCached.GetCameraProjUnjittered(eyeIndex).Transpose(); slConstants.cameraMotionIncluded = sl::Boolean::eTrue; slConstants.cameraPinholeOffset = { 0.f, 0.f }; slConstants.cameraRight = { viewMatrix._11, viewMatrix._12, viewMatrix._13 }; slConstants.cameraUp = { viewMatrix._21, viewMatrix._22, viewMatrix._23 }; slConstants.cameraFwd = { viewMatrix._31, viewMatrix._32, viewMatrix._33 }; - slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(); + slConstants.cameraPos = *(sl::float3*)&globals::game::frameBufferCached.GetCameraPosAdjust(eyeIndex); slConstants.cameraViewToClip = *(sl::float4x4*)&cameraViewToClip; slConstants.depthInverted = sl::Boolean::eFalse; - recalculateCameraMatrices(slConstants); + if (globals::game::isVR) { + // VR: compute clipToCameraView / clipToPrevClip / prevClipToClip from Skyrim's per-eye matrices. + // recalculateCameraMatrices() uses a single static prev-frame slot -- unusable for two viewports. + sl::matrixFullInvert(slConstants.clipToCameraView, slConstants.cameraViewToClip); + + auto currViewProj = globals::game::frameBufferCached.GetCameraViewProjUnjittered(eyeIndex).Transpose(); + auto prevViewProj = globals::game::frameBufferCached.GetCameraPreviousViewProjUnjittered(eyeIndex).Transpose(); + + sl::float4x4 currViewProjSL = *(sl::float4x4*)&currViewProj; + sl::float4x4 prevViewProjSL = *(sl::float4x4*)&prevViewProj; + + sl::float4x4 invCurrViewProj; + sl::matrixFullInvert(invCurrViewProj, currViewProjSL); + sl::matrixMul(slConstants.clipToPrevClip, invCurrViewProj, prevViewProjSL); + sl::matrixFullInvert(slConstants.prevClipToClip, slConstants.clipToPrevClip); + } else { + recalculateCameraMatrices(slConstants); + } auto& upscaling = globals::features::upscaling; auto jitter = upscaling.jitter; @@ -371,7 +394,7 @@ bool Streamline::CheckFrameConstants(sl::ViewportHandle p_viewport) slConstants.motionVectorsJittered = sl::Boolean::eFalse; if (SL_FAILED(res, slSetConstants(slConstants, *frameToken, p_viewport))) { - logger::error("[Streamline] Could not set constants"); + logger::error("[Streamline] Could not set constants for eye {}", eyeIndex); return false; } @@ -442,7 +465,10 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, u // produces zeroed output. See SetDLSSOptions decl in Streamline.h for the rationale. dlssOptions.outputHeight = height != 0 ? height : (dlssperfActive ? (uint)perfMode.GetDisplayScreenSize().y : (uint)state->screenSize.y); - // Detect HDR from kMAIN format at runtime + dlssOptions.outputWidth = width; + dlssOptions.outputHeight = (uint)state->screenSize.y; + + // Detect HDR from kMAIN format at runtime -- VR kMAIN may be 8-bit while SE is FP16 { auto renderer = globals::game::renderer; auto& main = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN]; @@ -500,7 +526,7 @@ void Streamline::SetDLSSOptions(sl::ViewportHandle p_viewport, uint32_t width, u } } -void Streamline::EvaluateDLSS(sl::ViewportHandle vp, +void Streamline::EvaluateDLSS(sl::ViewportHandle vp, uint32_t eyeIndex, ID3D11Resource* colorIn, ID3D11Resource* colorOut, ID3D11Resource* depth, ID3D11Resource* mvec, ID3D11Resource* reactiveMask, ID3D11Resource* transparencyMask, const sl::Extent& extentIn, const sl::Extent& extentOut, uint32_t outputWidth, @@ -515,7 +541,7 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, sl::Resource reactiveMaskRes = { sl::ResourceType::eTex2d, reactiveMask, 0 }; sl::Resource transparencyMaskRes = { sl::ResourceType::eTex2d, transparencyMask, 0 }; - if (!CheckFrameConstants(vp)) + if (!CheckFrameConstants(vp, eyeIndex)) return; const bool emitPCLMarkers = @@ -527,14 +553,16 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, return; const sl::Result markerResult = slPCLSetMarker(marker, *frameToken); if (markerResult != sl::Result::eOk) { - static bool markerErrorLogged[2] = { false, false }; + static bool markerErrorLogged[2][2] = { { false, false }, { false, false } }; + const uint32_t logIdx = globals::game::isVR ? std::min(eyeIndex, 1u) : 0u; const uint32_t boundedStageIndex = std::min(stageIndex, 1u); - if (markerErrorLogged[boundedStageIndex]) + if (markerErrorLogged[logIdx][boundedStageIndex]) return; - markerErrorLogged[boundedStageIndex] = true; + markerErrorLogged[logIdx][boundedStageIndex] = true; logger::warn( - "[Streamline] slPCLSetMarker({}) failed: {}", + "[Streamline] slPCLSetMarker({}) failed{}: {}", stageName, + globals::game::isVR ? std::format(" for eye {}", eyeIndex) : "", magic_enum::enum_name(markerResult)); } }; @@ -556,8 +584,15 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, const sl::BaseStructure* inputs[] = { &view }; auto state = globals::state; - if (state->frameAnnotations) - state->BeginPerfEvent("DLSS Evaluate"); + if (state->frameAnnotations) { + if (globals::game::isVR) { + char buf[32]; + snprintf(buf, sizeof(buf), "DLSS Evaluate Eye %u", eyeIndex); + state->BeginPerfEvent(buf); + } else { + state->BeginPerfEvent("DLSS Evaluate"); + } + } emitPCLMarker(sl::PCLMarker::eRenderSubmitStart, "DLSS-EvaluateStart", 0); sl::Result evalResult = slEvaluateFeature(sl::kFeatureDLSS, *frameToken, inputs, _countof(inputs), context); @@ -567,20 +602,23 @@ void Streamline::EvaluateDLSS(sl::ViewportHandle vp, state->EndPerfEvent(); if (evalResult != sl::Result::eOk) { - static bool evalErrorLogged = false; - if (!evalErrorLogged) { - evalErrorLogged = true; - logger::error("[Streamline] slEvaluateFeature failed result={}", (int)evalResult); + static bool evalErrorLogged[2] = { false, false }; + uint32_t logIdx = globals::game::isVR ? eyeIndex : 0; + if (!evalErrorLogged[logIdx]) { + evalErrorLogged[logIdx] = true; + logger::error("[Streamline] slEvaluateFeature failed{} result={}", globals::game::isVR ? std::format(" for eye {}", eyeIndex) : "", (int)evalResult); } } } void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_reactiveMask, ID3D11Resource* a_transparencyCompositionMask, ID3D11Resource* a_motionVectors) { + auto state = globals::state; + auto renderer = globals::game::renderer; auto& depthTexture = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; - float2 screenSize{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + auto screenSize = state->screenSize; auto renderSize = Util::ConvertToDynamic(screenSize); // PerfMode bridge: when the BSOpenVR size hook is live, state->screenSize @@ -602,8 +640,19 @@ void Streamline::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r dlssperfActive ? static_cast(perfMode.GetTestTexture()) : ((upscaling.settings.sharpnessDLSS > 0.0f && upscaling.sharpenerTexture) ? upscaling.sharpenerTexture->resource.get() : a_upscalingTexture); - sl::Extent extentIn{ 0, 0, (uint)renderSize.x, (uint)renderSize.y }; - sl::Extent extentOut{ 0, 0, (uint)screenSize.x, (uint)screenSize.y }; + // VR stereo DLSS: NGX D3D11 only accepts zero-offset subrects. Non-zero offsets return + // FAIL_InvalidParameter because Streamline's dlssEntry.cpp never sets + // NVSDK_NGX_Parameter_DLSS_Enable_Output_Subrects during context creation. + // + // Both eyes copy their color slice into per-eye intermediates so ClearHMDMask can zero + // outside-mask regions before DLSS sees them (prevents temporal bleed into visible pixels). + // Eye 0 outputs directly to colorOut (zero-offset) — no intermediate output buffer needed. + // Eye 1 outputs to vrIntermediateColorOut[1] then copies back to kMAIN at eyeWidthOut. + // + // Eye 1 is pre-copied before eye 0 runs: at non-DLAA scales eye 0's upscaled output + // extends past eyeWidthIn into eye 1's input region of kMAIN. + if (globals::game::isVR) { + auto context = globals::d3d::context; uint32_t eyeWidthOut = (uint32_t)(displaySize.x / 2); uint32_t eyeHeightOut = (uint32_t)displaySize.y; @@ -762,4 +811,9 @@ void Streamline::DestroyDLSSResources() slDLSSSetOptions(viewport, dlssOptions); slFreeResources(sl::kFeatureDLSS, viewport); + + if (globals::game::isVR) { + slDLSSSetOptions(viewportRight, dlssOptions); + slFreeResources(sl::kFeatureDLSS, viewportRight); + } } diff --git a/src/Features/Upscaling/Streamline.h b/src/Features/Upscaling/Streamline.h index 421a261703..ea8088f7bf 100644 --- a/src/Features/Upscaling/Streamline.h +++ b/src/Features/Upscaling/Streamline.h @@ -37,6 +37,7 @@ class Streamline bool reflexSupportedOnCurrentAdapter = false; sl::ViewportHandle viewport{ 0 }; + sl::ViewportHandle viewportRight{ 1 }; static constexpr uint32_t MAX_RESOLUTION = 8192; HMODULE interposer = NULL; @@ -107,7 +108,7 @@ class Streamline void PostDevice(); bool EnsureFrameToken(); - bool CheckFrameConstants(sl::ViewportHandle p_viewport); + bool CheckFrameConstants(sl::ViewportHandle p_viewport, uint32_t eyeIndex = 0); bool IsRTXAndBelow40Series(IDXGIAdapter* a_adapter); diff --git a/src/Features/VR.cpp b/src/Features/VR.cpp index 13d2111d0d..d38a5cb9e9 100644 --- a/src/Features/VR.cpp +++ b/src/Features/VR.cpp @@ -1,4 +1,4 @@ -#include "VR.h" +#include "VR.h" #include "Menu.h" #include "RE/B/BSOpenVR.h" #include "RE/P/PlayerCharacter.h" diff --git a/src/Features/VR/InSceneOverlay.cpp b/src/Features/VR/InSceneOverlay.cpp new file mode 100644 index 0000000000..30f04e9367 --- /dev/null +++ b/src/Features/VR/InSceneOverlay.cpp @@ -0,0 +1,612 @@ +#include "Features/VR.h" +#include "Globals.h" +#include "Hooks.h" +#include "Menu.h" +#include "Util.h" +#include "Utils/VRUtils.h" +#include +#include +#include +#include +#include +#include + +using namespace DirectX; +using namespace DirectX::SimpleMath; + +using AttachMode = VR::Settings::OverlayAttachMode; + +//============================================================================= +// IN-SCENE OVERLAY RENDERING VIA SUBMIT HOOK +//============================================================================= + +namespace +{ + struct IVRCompositor_Submit + { + static vr::EVRCompositorError thunk(vr::IVRCompositor* _this, vr::EVREye eEye, const vr::Texture_t* pTexture, const vr::VRTextureBounds_t* pBounds, vr::EVRSubmitFlags nSubmitFlags) + { + auto& vr = globals::features::vr; + // Only process DirectX textures - skip OpenGL/Vulkan to avoid undefined behavior + if (pTexture && pTexture->handle && pTexture->eType == vr::TextureType_DirectX) { + vr.RenderInSceneOverlay(eEye, (ID3D11Texture2D*)pTexture->handle, pBounds); + } + return func(_this, eEye, pTexture, pBounds, nSubmitFlags); + } + static inline REL::Relocation func; + }; +} + +void VR::InitInSceneResources() +{ + if (inSceneResources.initialized) + return; + + InSceneResources temp = {}; + + auto device = globals::d3d::device; + + // 1. Compile shaders - compile VS to get bytecode for input layout, PS separately + ID3DBlob* vsBlob = nullptr; + ID3DBlob* psBlob = nullptr; + ID3DBlob* errorBlob = nullptr; + + // Compile vertex shader + if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.vs.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, + "main", "vs_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vsBlob, &errorBlob))) { + if (errorBlob) { + logger::error("VR InScene VS compile error: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + return; + } + if (errorBlob) { + errorBlob->Release(); + errorBlob = nullptr; + } + + // Compile pixel shader + if (FAILED(D3DCompileFromFile(L"Data\\Shaders\\VR\\InSceneOverlay.ps.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, + "main", "ps_5_0", D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &psBlob, &errorBlob))) { + if (errorBlob) { + logger::error("VR InScene PS compile error: {}", (char*)errorBlob->GetBufferPointer()); + errorBlob->Release(); + } + if (vsBlob) + vsBlob->Release(); + return; + } + if (errorBlob) { + errorBlob->Release(); + errorBlob = nullptr; + } + + // Create shader objects from bytecode + ID3D11VertexShader* vs = nullptr; + ID3D11PixelShader* ps = nullptr; + if (FAILED(device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &vs)) || + FAILED(device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &ps))) { + logger::error("VR: Failed to create shader objects"); + if (vs) + vs->Release(); + if (ps) + ps->Release(); + if (vsBlob) + vsBlob->Release(); + if (psBlob) + psBlob->Release(); + return; + } + + temp.vs.attach(vs); + temp.ps.attach(ps); + if (psBlob) + psBlob->Release(); // Don't need PS blob anymore + + // 2. Input Layout + D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; + polygonLayout[0].SemanticName = "POSITION"; + polygonLayout[0].SemanticIndex = 0; + polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; + polygonLayout[0].InputSlot = 0; + polygonLayout[0].AlignedByteOffset = 0; + polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; + polygonLayout[0].InstanceDataStepRate = 0; + + polygonLayout[1].SemanticName = "TEXCOORD"; + polygonLayout[1].SemanticIndex = 0; + polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; + polygonLayout[1].InputSlot = 0; + polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; + polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; + polygonLayout[1].InstanceDataStepRate = 0; + + if (FAILED(device->CreateInputLayout(polygonLayout, 2, vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), temp.inputLayout.put()))) { + logger::error("VR: Failed to create input layout"); + vsBlob->Release(); + return; + } + + vsBlob->Release(); + + // 3. Buffers + // Quad Vertices (XY plane, z=0, size=1) + struct VertexType + { + XMFLOAT3 position; + XMFLOAT2 texture; + }; + VertexType vertices[4] = { + { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT2(0.0f, 1.0f) }, // Bottom Left + { XMFLOAT3(-0.5f, 0.5f, 0.0f), XMFLOAT2(0.0f, 0.0f) }, // Top Left + { XMFLOAT3(0.5f, 0.5f, 0.0f), XMFLOAT2(1.0f, 0.0f) }, // Top Right + { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT2(1.0f, 1.0f) } // Bottom Right + }; + + D3D11_BUFFER_DESC vertexBufferDesc = {}; + vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; + vertexBufferDesc.ByteWidth = sizeof(VertexType) * 4; + vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + D3D11_SUBRESOURCE_DATA vertexData = {}; + vertexData.pSysMem = vertices; + if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, temp.vb.put()))) { + logger::error("VR: Failed to create vertex buffer"); + return; + } + + unsigned long indices[6] = { 0, 1, 2, 0, 2, 3 }; + D3D11_BUFFER_DESC indexBufferDesc = {}; + indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; + indexBufferDesc.ByteWidth = sizeof(unsigned long) * 6; + indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; + D3D11_SUBRESOURCE_DATA indexData = {}; + indexData.pSysMem = indices; + if (FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, temp.ib.put()))) { + logger::error("VR: Failed to create index buffer"); + return; + } + + D3D11_BUFFER_DESC matrixBufferDesc = {}; + matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; + matrixBufferDesc.ByteWidth = sizeof(InSceneCB); + matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + if (FAILED(device->CreateBuffer(&matrixBufferDesc, nullptr, temp.cb.put()))) { + logger::error("VR: Failed to create constant buffer"); + return; + } + + // 4. States + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = 0x0F; + if (FAILED(device->CreateBlendState(&blendDesc, temp.blendState.put()))) { + logger::error("VR: Failed to create blend state"); + return; + } + + D3D11_DEPTH_STENCIL_DESC depthDesc = {}; + depthDesc.DepthEnable = FALSE; // Always on top + depthDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO; + depthDesc.DepthFunc = D3D11_COMPARISON_ALWAYS; + if (FAILED(device->CreateDepthStencilState(&depthDesc, temp.depthState.put()))) { + logger::error("VR: Failed to create depth stencil state"); + return; + } + + D3D11_RASTERIZER_DESC rasterDesc = {}; + rasterDesc.FillMode = D3D11_FILL_SOLID; + rasterDesc.CullMode = D3D11_CULL_NONE; + rasterDesc.FrontCounterClockwise = FALSE; + rasterDesc.DepthClipEnable = TRUE; + if (FAILED(device->CreateRasterizerState(&rasterDesc, temp.rasterizerState.put()))) { + logger::error("VR: Failed to create rasterizer state"); + return; + } + + D3D11_SAMPLER_DESC samplerDesc = {}; + samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + samplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; + samplerDesc.MinLOD = 0; + samplerDesc.MaxLOD = D3D11_FLOAT32_MAX; + if (FAILED(device->CreateSamplerState(&samplerDesc, temp.sampler.put()))) { + logger::error("VR: Failed to create sampler state"); + return; + } + Util::SetResourceName(temp.sampler.get(), "VR::InSceneOverlaySampler"); + + inSceneResources = std::move(temp); + inSceneResources.initialized = true; + logger::debug("VR: In-Scene Overlay resources initialized."); +} + +void VR::RenderInSceneOverlay(vr::EVREye eye, ID3D11Texture2D* targetTexture, const vr::VRTextureBounds_t* bounds) +{ + if (!globals::menu || !(globals::menu->IsEnabled || globals::menu->overlayVisible || IsWelcomeOverlayVisible()) || settings.attachMode == AttachMode::None || !menuTexture) { + return; + } + + auto context = globals::d3d::context; + winrt::com_ptr perf; + context->QueryInterface(__uuidof(ID3DUserDefinedAnnotation), perf.put_void()); + + static const wchar_t* eventNames[] = { L"VR In-Scene Overlay (Eye 0)", L"VR In-Scene Overlay (Eye 1)" }; + if (perf) + perf->BeginEvent(eventNames[(int)eye]); + + if (!inSceneResources.initialized) + InitInSceneResources(); + if (!inSceneResources.initialized) { + if (perf) + perf->EndEvent(); + return; + } + + // We can't render if we don't have HMD pose + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) { + if (perf) + perf->EndEvent(); + return; + } + + // Get HMD Pose and Eye matrices + vr::TrackedDevicePose_t hmdPose; + vr::TrackedDevicePose_t renderPose[vr::k_unMaxTrackedDeviceCount]; + + RE::BSOpenVR::GetIVRCompositor()->GetLastPoses(renderPose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + hmdPose = renderPose[vr::k_unTrackedDeviceIndex_Hmd]; + if (!hmdPose.bPoseIsValid) { + if (perf) + perf->EndEvent(); + return; + } + + Matrix hmdWorld = Matrix::Identity; + Matrix eyeToHead = Matrix::Identity; + Matrix proj = Matrix::Identity; + Matrix vpHeadSpace = Matrix::Identity; // For HMD-relative rendering (head space) + Matrix vpWorldSpace = Matrix::Identity; // For world/controller rendering (world space) + + // Always get Eye and Projection matrices + eyeToHead = Util::HmdMatrix34ToMatrix(openvr->vrSystem->GetEyeToHeadTransform(eye)); + + // Use GetProjectionRaw to build a DirectX-compatible projection matrix (Depth [0, 1]) + // IMPORTANT: OpenVR GetProjectionRaw has a known bug (Valve issue #110, open since 2016): + // The 3rd parameter (named "pTop") actually returns the BOTTOM tangent, and + // the 4th parameter (named "pBottom") actually returns the TOP tangent. + // We name our variables to match the ACTUAL values, not the misleading parameter names. + float left, right, bottom, top; + openvr->vrSystem->GetProjectionRaw(eye, &left, &right, &bottom, &top); + float nearZ = 0.1f; + float farZ = 1000.0f; + + proj = DirectX::XMMatrixPerspectiveOffCenterRH(left * nearZ, right * nearZ, bottom * nearZ, top * nearZ, nearZ, farZ); + + // Log projection values once per eye + static bool projLogged[2] = { false, false }; + if (!projLogged[(int)eye]) { + logger::debug("VR Projection Eye {}: L={:.4f} R={:.4f} B={:.4f} T={:.4f}, EyeX={:.4f}", + (int)eye, left, right, bottom, top, eyeToHead._41); + projLogged[(int)eye] = true; + } + + // Head-space VP (for HMD-relative mode) + vpHeadSpace = eyeToHead.Invert() * proj; + + // World-space VP (for controller attach and fixed world position modes) + if (hmdPose.bPoseIsValid) { + hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + // Transform chain: eye → head → world (row-vector: left-to-right composition) + Matrix eyeToWorld = eyeToHead * hmdWorld; + vpWorldSpace = eyeToWorld.Invert() * proj; + } + + // Get or create cached RTV for the target texture + D3D11_TEXTURE2D_DESC texDesc; + targetTexture->GetDesc(&texDesc); + + int eyeIdx = (int)eye; + auto& cachedRTV = inSceneResources.cachedEyeRTVs[eyeIdx]; + if (cachedRTV.texture != targetTexture) { + cachedRTV.rtv = nullptr; + cachedRTV.texture = nullptr; + + D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {}; + rtvDesc.Format = texDesc.Format; + + if (texDesc.ArraySize > 1) { + if (texDesc.SampleDesc.Count > 1) { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY; + rtvDesc.Texture2DMSArray.FirstArraySlice = (UINT)eye; + rtvDesc.Texture2DMSArray.ArraySize = 1; + } else { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY; + rtvDesc.Texture2DArray.FirstArraySlice = (UINT)eye; + rtvDesc.Texture2DArray.ArraySize = 1; + rtvDesc.Texture2DArray.MipSlice = 0; + } + } else if (texDesc.SampleDesc.Count > 1) { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS; + } else { + rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + rtvDesc.Texture2D.MipSlice = 0; + } + + HRESULT hr = globals::d3d::device->CreateRenderTargetView(targetTexture, &rtvDesc, cachedRTV.rtv.put()); + if (FAILED(hr)) { + logger::error("VR: Failed to create RTV for eye texture (Format: {}, Samples: {}). HRESULT: {:x}", + (uint32_t)texDesc.Format, texDesc.SampleDesc.Count, (uint32_t)hr); + if (perf) + perf->EndEvent(); + return; + } + cachedRTV.texture = targetTexture; + } + + auto& rtv = cachedRTV.rtv; + + // Save State + ID3D11RenderTargetView* oldRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT]; + ID3D11DepthStencilView* oldDSV; + context->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, &oldDSV); + + D3D11_VIEWPORT oldViewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE]; + UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE; + context->RSGetViewports(&numViewports, oldViewports); + + ID3D11RasterizerState* oldRS = nullptr; + context->RSGetState(&oldRS); + + ID3D11BlendState* oldBlend = nullptr; + FLOAT oldBlendFactor[4]; + UINT oldSampleMask; + context->OMGetBlendState(&oldBlend, oldBlendFactor, &oldSampleMask); + + ID3D11DepthStencilState* oldDepth = nullptr; + UINT oldStencilRef; + context->OMGetDepthStencilState(&oldDepth, &oldStencilRef); + + // Setup Render + ID3D11RenderTargetView* rtvPtr = rtv.get(); + context->OMSetRenderTargets(1, &rtvPtr, nullptr); // No DSV + + // Viewport: Use bounds if provided (for SBS textures), otherwise use full texture + D3D11_VIEWPORT vpDesc = {}; + if (bounds) { + vpDesc.TopLeftX = bounds->uMin * texDesc.Width; + vpDesc.TopLeftY = bounds->vMin * texDesc.Height; + vpDesc.Width = (bounds->uMax - bounds->uMin) * texDesc.Width; + vpDesc.Height = (bounds->vMax - bounds->vMin) * texDesc.Height; + } else { + vpDesc.TopLeftX = 0.0f; + vpDesc.TopLeftY = 0.0f; + vpDesc.Width = (float)texDesc.Width; + vpDesc.Height = (float)texDesc.Height; + } + vpDesc.MinDepth = 0.0f; + vpDesc.MaxDepth = 1.0f; + context->RSSetViewports(1, &vpDesc); + + // Log texture and viewport details once per eye per session + static bool textureInfoLogged[2] = { false, false }; + if (!textureInfoLogged[eyeIdx]) { + logger::debug("VR Submit Texture Info (Eye {}):", eyeIdx); + logger::debug(" Texture Size: {}x{}, Format: {}, ArraySize: {}, SampleCount: {}", + texDesc.Width, texDesc.Height, (uint32_t)texDesc.Format, texDesc.ArraySize, texDesc.SampleDesc.Count); + if (bounds) { + logger::debug(" Bounds: uMin={:.3f}, vMin={:.3f}, uMax={:.3f}, vMax={:.3f}", + bounds->uMin, bounds->vMin, bounds->uMax, bounds->vMax); + logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", + vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); + } else { + logger::debug(" No bounds provided (full texture per eye, or texture array)"); + logger::debug(" Viewport: X={:.0f}, Y={:.0f}, W={:.0f}, H={:.0f}", + vpDesc.TopLeftX, vpDesc.TopLeftY, vpDesc.Width, vpDesc.Height); + } + logger::debug(" RTV Dimension: {}", + (texDesc.ArraySize > 1 && texDesc.SampleDesc.Count > 1) ? "Texture2DMSArray" : + (texDesc.ArraySize > 1) ? "Texture2DArray (per-eye slice)" : + (texDesc.SampleDesc.Count > 1) ? "Texture2DMS" : + "Texture2D (single)"); + textureInfoLogged[eyeIdx] = true; + } + + // Helper to draw the overlay quad with a given WVP matrix + auto drawOverlayQuad = [&](ID3D11DeviceContext* ctx, const InSceneCB& cbData) { + D3D11_MAPPED_SUBRESOURCE mappedResource; + if (SUCCEEDED(ctx->Map(inSceneResources.cb.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource))) { + memcpy(mappedResource.pData, &cbData, sizeof(InSceneCB)); + ctx->Unmap(inSceneResources.cb.get(), 0); + } + + ctx->VSSetShader(inSceneResources.vs.get(), nullptr, 0); + ctx->PSSetShader(inSceneResources.ps.get(), nullptr, 0); + ID3D11Buffer* cb = inSceneResources.cb.get(); + ctx->VSSetConstantBuffers(0, 1, &cb); + + struct VT + { + XMFLOAT3 p; + XMFLOAT2 t; + }; + UINT stride = sizeof(VT); + UINT offset = 0; + ID3D11Buffer* vb = inSceneResources.vb.get(); + ctx->IASetVertexBuffers(0, 1, &vb, &stride, &offset); + ctx->IASetIndexBuffer(inSceneResources.ib.get(), DXGI_FORMAT_R32_UINT, 0); + ctx->IASetInputLayout(inSceneResources.inputLayout.get()); + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + ctx->OMSetBlendState(inSceneResources.blendState.get(), nullptr, 0xFFFFFFFF); + ctx->OMSetDepthStencilState(inSceneResources.depthState.get(), 0); + ctx->RSSetState(inSceneResources.rasterizerState.get()); + + // Cache SRV to avoid creating every frame + if (menuTexture.get() != inSceneResources.cachedMenuTexture) { + inSceneResources.menuSRV = nullptr; + if (FAILED(globals::d3d::device->CreateShaderResourceView(menuTexture.get(), nullptr, inSceneResources.menuSRV.put()))) { + logger::error("VR: Failed to create menu texture SRV"); + return; + } + inSceneResources.cachedMenuTexture = menuTexture.get(); + } + ID3D11ShaderResourceView* srvPtr = inSceneResources.menuSRV.get(); + ctx->PSSetShaderResources(0, 1, &srvPtr); + + ID3D11SamplerState* sampler = inSceneResources.sampler.get(); + ctx->PSSetSamplers(0, 1, &sampler); + + ctx->DrawIndexed(6, 0, 0); + }; + + // --- Render HMD Overlay --- + if ((settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) && menuTexture) { + InSceneCB cbData; + + Matrix modelMatrix; + Matrix vp; + if (settings.VRMenuPositioningMethod == 1) { // Fixed World Position + modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * fixedWorldOverlayPosition.m; + vp = vpWorldSpace; + } else { // HMD Relative + Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); + modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * offset; + vp = vpHeadSpace; + } + cbData.wvp = (modelMatrix * vp).Transpose(); + + drawOverlayQuad(context, cbData); + } + + // --- Render Controller Overlay --- + if ((settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) && menuTexture) { + vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachIndex != vr::k_unTrackedDeviceIndexInvalid && attachIndex < vr::k_unMaxTrackedDeviceCount) { + vr::TrackedDevicePose_t controllerPose = renderPose[attachIndex]; + if (controllerPose.bPoseIsValid) { + Matrix controllerWorld = Util::HmdMatrix34ToMatrix(controllerPose.mDeviceToAbsoluteTracking); + Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); + Matrix modelMatrix = VR::Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * offset * controllerWorld; + + // Backface culling: hide overlay when viewed from behind + // Use the unscaled controller+offset transform for correct normal direction + Matrix overlayTransform = offset * controllerWorld; + Vector3 overlayNormal(overlayTransform._31, overlayTransform._32, overlayTransform._33); + overlayNormal.Normalize(); + Matrix eyeWorld = eyeToHead * hmdWorld; + Vector3 eyePos = eyeWorld.Translation(); + Vector3 overlayPos = overlayTransform.Translation(); + Vector3 toEye = eyePos - overlayPos; + toEye.Normalize(); + // Quad front face is +Z in local space (D3D default CW winding). + // Render when eye is on the +Z side of the overlay (dot > 0). + float dot = overlayNormal.Dot(toEye); + if (dot > 0.0f) { + InSceneCB cbData; + cbData.wvp = (modelMatrix * vpWorldSpace).Transpose(); + drawOverlayQuad(context, cbData); + } + } + } + } + + // Restore State + context->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, oldRTVs, oldDSV); + context->RSSetViewports(numViewports, oldViewports); + context->OMSetBlendState(oldBlend, oldBlendFactor, oldSampleMask); + context->OMSetDepthStencilState(oldDepth, oldStencilRef); + if (oldRS) { + context->RSSetState(oldRS); + oldRS->Release(); + } + if (oldBlend) + oldBlend->Release(); + if (oldDepth) + oldDepth->Release(); + for (int i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) + if (oldRTVs[i]) + oldRTVs[i]->Release(); + if (oldDSV) + oldDSV->Release(); + + if (perf) + perf->EndEvent(); +} + +void VR::InstallSubmitHook() +{ + static bool installed = false; + if (installed) + return; + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (openvr && RE::BSOpenVR::GetIVRCompositor()) { + logger::info("VR: Installing IVRCompositor::Submit hook for in-scene overlay rendering"); + + // Log comprehensive VR system parameters (debug only) + logger::debug("=== VR System Configuration ==="); + + // Get and log IPD + float ipd = Util::GetIPDFromHMD(); + logger::debug("IPD: {:.4f} meters ({:.2f} mm)", ipd, ipd * 1000.0f); + + // Get and log eye transforms + if (openvr->vrSystem) { + vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); + vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); + + logger::debug("Left Eye Transform:"); + logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", + leftEye.m[0][3], leftEye.m[1][3], leftEye.m[2][3]); + logger::debug("Right Eye Transform:"); + logger::debug(" Translation: X={:.4f}, Y={:.4f}, Z={:.4f}", + rightEye.m[0][3], rightEye.m[1][3], rightEye.m[2][3]); + logger::debug("Calculated Eye Separation: {:.4f} meters ({:.2f} mm)", + std::abs(leftEye.m[0][3] - rightEye.m[0][3]), + std::abs(leftEye.m[0][3] - rightEye.m[0][3]) * 1000.0f); + + // Get projection matrices + vr::HmdMatrix44_t leftProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); + vr::HmdMatrix44_t rightProj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Right, 0.1f, 1000.0f); + + logger::debug("Projection Matrices (near=0.1, far=1000.0):"); + logger::debug(" Left [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", + leftProj.m[0][0], leftProj.m[1][1], leftProj.m[0][2]); + logger::debug(" Right [0][0]={:.4f}, [1][1]={:.4f}, [0][2]={:.4f}", + rightProj.m[0][0], rightProj.m[1][1], rightProj.m[0][2]); + } + + logger::debug("Convergence Formula Info:"); + logger::debug(" Formula: stereoShift = (IPD/2) / (depth * tan(hFOV/2))"); + logger::debug(" - Shift is independent of scale (scale only controls size)"); + logger::debug(" - Depth is controlled by OffsetZ (negative = in front)"); + float halfIPD = ipd / 2.0f; + if (openvr->vrSystem) { + vr::HmdMatrix44_t proj = openvr->vrSystem->GetProjectionMatrix(vr::Eye_Left, 0.1f, 1000.0f); + float tanHFOV = 1.0f / proj.m[0][0]; + logger::debug(" tan(hFOV/2) = {:.4f}", tanHFOV); + logger::debug(" Example: At depth 1.0m, shift={:.6f}", halfIPD / (1.0f * tanHFOV)); + logger::debug(" Example: At depth 2.0m, shift={:.6f}", halfIPD / (2.0f * tanHFOV)); + logger::debug(" Example: At depth 5.0m, shift={:.6f}", halfIPD / (5.0f * tanHFOV)); + } + logger::debug("================================"); + + // IVRCompositor::Submit is index 5 + stl::detour_vfunc<5, IVRCompositor_Submit>(RE::BSOpenVR::GetIVRCompositor()); + installed = true; + + logger::info("VR: In-scene overlay initialized"); + } else { + logger::warn("VR: Failed to install IVRCompositor::Submit hook - Interface not available"); + } +} diff --git a/src/Features/VR/OverlayDrag.cpp b/src/Features/VR/OverlayDrag.cpp new file mode 100644 index 0000000000..bd0180b33c --- /dev/null +++ b/src/Features/VR/OverlayDrag.cpp @@ -0,0 +1,405 @@ +#include "Features/VR.h" +#include "RE/B/BSOpenVR.h" +#include "RE/P/PlayerCharacter.h" +#include "Utils/VRUtils.h" + +#include +#include +#include +#include +#include + +using namespace DirectX::SimpleMath; +using AttachMode = VR::Settings::OverlayAttachMode; + +bool VR::GetGripPressed(bool isLeft, bool isRight) const +{ + bool isLeftHanded = lastKnownLeftHandedMode; + + if (isLeft) { + if (isLeftHanded) { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + if (isRight) { + if (isLeftHanded) { + return secondaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } else { + return primaryControllerState[RE::BSOpenVRControllerDevice::Keys::kGrip].isPressed; + } + } + return false; +} + +static bool CanStartAny(vr::ETrackedControllerRole role) +{ + return role != vr::TrackedControllerRole_Invalid; +} + +void VR::UpdateOverlayDrag() +{ + if (!CanPerformDrag()) { + return; + } + + if (overlayDragState.dragging) { + UpdateActiveDrag(); + } else { + TryStartNewDrag(); + } +} + +bool VR::CanPerformDrag() +{ + if (!settings.EnableDragToReposition) + return false; + + if (!globals::menu || !globals::menu->IsEnabled) + return false; + + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return false; + + if (settings.VRMenuControllerDiagnosticsTestMode) { + return false; + } + + return true; +} + +void VR::UpdateActiveDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + auto resetDragState = [&]() { + overlayDragState.dragging = false; + overlayDragState.controllerIndex = vr::k_unTrackedDeviceIndexInvalid; + overlayDragState.isPrimary = false; + overlayDragState.isSecondary = false; + }; + + float rawMatrix[3][4]; + if (Util::GetControllerWorldMatrix(overlayDragState.controllerIndex, rawMatrix)) { + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + + switch (overlayDragState.mode) { + case OverlayDragState::DragMode::Controller: + { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + float attachedM[3][4]; + if (!Util::GetControllerWorldMatrix(attachedControllerIndex, attachedM)) + break; + { + Matrix attachedControllerMatrix = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachedM)); + + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + + Matrix worldToLocal = attachedControllerMatrix.Invert(); + Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); + + settings.VRMenuControllerOffsetX = overlayDragState.initialControllerOffset.x + localDelta.x; + settings.VRMenuControllerOffsetY = overlayDragState.initialControllerOffset.y + localDelta.y; + settings.VRMenuControllerOffsetZ = overlayDragState.initialControllerOffset.z + localDelta.z; + } + } + break; + } + case OverlayDragState::DragMode::FixedWorld: + { + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + Matrix translated = overlayDragState.initialOverlayMatrix; + translated._41 += worldDelta.x; + translated._42 += worldDelta.y; + translated._43 += worldDelta.z; + fixedWorldOverlayPosition.m = translated; + break; + } + case OverlayDragState::DragMode::HMD: + { + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + break; + if (hmdPose.bPoseIsValid) { + Matrix hmdMatrix = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + + Vector3 worldDelta( + controllerMatrix._41 - overlayDragState.initialControllerMatrix._41, + controllerMatrix._42 - overlayDragState.initialControllerMatrix._42, + controllerMatrix._43 - overlayDragState.initialControllerMatrix._43); + + Matrix worldToLocal = hmdMatrix.Invert(); + Vector3 localDelta = Vector3::TransformNormal(worldDelta, worldToLocal); + + static auto lastDeltaLog = std::chrono::steady_clock::now(); + auto nowDelta = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(nowDelta - lastDeltaLog).count() > 500) { + logger::debug("VR Drag Delta - Local: ({:.3f}, {:.3f}, {:.3f})", localDelta.x, localDelta.y, localDelta.z); + lastDeltaLog = nowDelta; + } + + settings.VRMenuOffsetX = overlayDragState.initialHMDOffset.x + localDelta.x; + settings.VRMenuOffsetY = overlayDragState.initialHMDOffset.y + localDelta.y; + settings.VRMenuOffsetZ = overlayDragState.initialHMDOffset.z + localDelta.z; + settings.VRMenuScale = overlayDragState.initialHMDScale; + + static std::chrono::steady_clock::time_point lastLog = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastLog).count() > 500) { + logger::debug("VR Dragging (3D Mode): Offset ({:.2f}, {:.2f}, {:.2f}), Scale {:.2f}", + settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ, settings.VRMenuScale); + lastLog = now; + } + } + break; + } + default: + break; + } + } + + // Joystick depth control during grip + if (overlayDragState.dragging) { + RE::VRControllerState* gripController = nullptr; + size_t thumbIdx = 0; + if (overlayDragState.isPrimary) { + if (lastKnownLeftHandedMode) { + gripController = &primaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Primary); + } else { + gripController = &secondaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Secondary); + } + } else if (overlayDragState.isSecondary) { + if (lastKnownLeftHandedMode) { + gripController = &secondaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Secondary); + } else { + gripController = &primaryControllerState; + thumbIdx = static_cast(RE::ControllerRole::Primary); + } + } + + if (gripController) { + float thumbY = gripController->thumbsticks[thumbIdx].y; + const float deadzone = settings.mouseDeadzone; + const float depthSpeed = 0.02f; + if (std::abs(thumbY) > deadzone) { + float depthDelta = -thumbY * depthSpeed; + if (overlayDragState.mode == OverlayDragState::DragMode::HMD) { + overlayDragState.initialHMDOffset.z += depthDelta; + overlayDragState.initialHMDOffset.z = std::clamp(overlayDragState.initialHMDOffset.z, -10.0f, 10.0f); + } else if (overlayDragState.mode == OverlayDragState::DragMode::Controller) { + overlayDragState.initialControllerOffset.z += depthDelta; + overlayDragState.initialControllerOffset.z = std::clamp(overlayDragState.initialControllerOffset.z, -10.0f, 10.0f); + } + } + } + } + + bool gripPressed = GetGripPressed(overlayDragState.isPrimary, overlayDragState.isSecondary); + if (!gripPressed) { + resetDragState(); + } +} + +void VR::TryStartNewDrag() +{ + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + auto* system = openvr ? openvr->vrSystem : nullptr; + if (!system) + return; + + struct DragMode + { + OverlayDragState::DragMode mode; + bool isActive; + std::function canStart; + std::function onInit; + }; + + std::vector dragModes; + + // Controller mode - only for opposite hand (highest priority) + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + dragModes.push_back({ OverlayDragState::DragMode::Controller, + true, + [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + ControllerDevice oppositeDevice = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + vr::TrackedDeviceIndex_t oppositeControllerIndex = Util::GetControllerIndexForDevice(oppositeDevice, lastKnownLeftHandedMode); + if (oppositeControllerIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(i); + if (deviceRole == role && i == oppositeControllerIndex) + return true; + } + } + return false; + }, + [&]() { + overlayDragState.initialControllerOffset.x = settings.VRMenuControllerOffsetX; + overlayDragState.initialControllerOffset.y = settings.VRMenuControllerOffsetY; + overlayDragState.initialControllerOffset.z = settings.VRMenuControllerOffsetZ; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + // Fixed world mode + if (settings.VRMenuPositioningMethod == 1) { + std::function fixedWorldCanStart; + if (settings.attachMode == AttachMode::Both) { + fixedWorldCanStart = [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + return role == actualAttachedRole; + } + return false; + }; + } else { + fixedWorldCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::FixedWorld, + true, + fixedWorldCanStart, + [&]() { + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + overlayDragState.initialOverlayMatrix = fixedWorldOverlayPosition.m; + } }); + } + + // HMD mode + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + std::function hmdCanStart; + if (settings.attachMode == AttachMode::Both) { + hmdCanStart = [&](vr::ETrackedControllerRole role) { + vr::TrackedDeviceIndex_t attachedControllerIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachedControllerIndex != vr::k_unTrackedDeviceIndexInvalid) { + vr::ETrackedControllerRole actualAttachedRole = system->GetControllerRoleForTrackedDeviceIndex(attachedControllerIndex); + return role == actualAttachedRole; + } + return false; + }; + } else { + hmdCanStart = CanStartAny; + } + + dragModes.push_back({ OverlayDragState::DragMode::HMD, + true, + hmdCanStart, + [&]() { + overlayDragState.initialHMDOffset.x = settings.VRMenuOffsetX; + overlayDragState.initialHMDOffset.y = settings.VRMenuOffsetY; + overlayDragState.initialHMDOffset.z = settings.VRMenuOffsetZ; + overlayDragState.initialHMDScale = settings.VRMenuScale; + overlayDragState.initialControllerMatrix = overlayDragState.startControllerMatrix; + } }); + } + + for (const auto& mode : dragModes) { + if (!mode.isActive) + continue; + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (system->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) + continue; + vr::ETrackedControllerRole role = system->GetControllerRoleForTrackedDeviceIndex(i); + bool isLeft = (role == vr::ETrackedControllerRole::TrackedControllerRole_LeftHand); + bool isRight = (role == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (!mode.canStart(role)) + continue; + bool gripPressed = GetGripPressed(isLeft, isRight); + if (!gripPressed) + continue; + float rawMatrix[3][4]; + if (!Util::GetControllerWorldMatrix(i, rawMatrix)) + continue; + vr::HmdMatrix34_t mat = Util::Float3x4ToHmdMatrix34(rawMatrix); + Matrix controllerMatrix = Util::HmdMatrix34ToMatrix(mat); + overlayDragState.dragging = true; + overlayDragState.mode = mode.mode; + overlayDragState.controllerIndex = i; + overlayDragState.isPrimary = isLeft; + overlayDragState.isSecondary = isRight; + overlayDragState.startControllerMatrix = controllerMatrix; + mode.onInit(); + + if (system && globals::menu->IsEnabled) { + for (vr::TrackedDeviceIndex_t deviceIdx = 0; deviceIdx < vr::k_unMaxTrackedDeviceCount; ++deviceIdx) { + if (system->GetTrackedDeviceClass(deviceIdx) == vr::TrackedDeviceClass_Controller) { + vr::ETrackedControllerRole deviceRole = system->GetControllerRoleForTrackedDeviceIndex(deviceIdx); + bool isRightController = (deviceRole == vr::ETrackedControllerRole::TrackedControllerRole_RightHand); + if (isRightController == isRight) { + openvr->TriggerHapticPulse(isRightController, 25.0f); + break; + } + } + } + } + + return; + } + } +} + +void VR::SetFixedOverlayToCurrentHMD() +{ + vr::HmdMatrix34_t transform = Util::ComputeOverlayTransformFromHMD( + settings.VRMenuOffsetX, + settings.VRMenuOffsetY, + settings.VRMenuOffsetZ); + fixedWorldOverlayPosition.m = Util::HmdMatrix34ToMatrix(transform); +} + +void VR::UpdateFixedWorldPositioning() +{ + if (settings.VRMenuPositioningMethod != 1) + return; + + if (!fixedWorldOverlayPosition.initialized) { + fixedWorldOverlayPosition.initialized = true; + SetFixedOverlayToCurrentHMD(); + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) { + savedPlayerWorldPos = player->GetPosition(); + } + return; + } + + if (settings.VRMenuAutoResetDistance > 0.0f) { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player) { + RE::NiPoint3 playerPos = player->GetPosition(); + float sqDist = playerPos.GetSquaredDistance(savedPlayerWorldPos); + float thresholdSq = settings.VRMenuAutoResetDistance * settings.VRMenuAutoResetDistance; + if (sqDist > thresholdSq) { + SetFixedOverlayToCurrentHMD(); + savedPlayerWorldPos = playerPos; + } + } + } +} diff --git a/src/Features/VR/WandPointing.cpp b/src/Features/VR/WandPointing.cpp new file mode 100644 index 0000000000..2b748bba28 --- /dev/null +++ b/src/Features/VR/WandPointing.cpp @@ -0,0 +1,146 @@ +#include "Features/VR.h" +#include "RE/B/BSOpenVR.h" +#include "Utils/VRUtils.h" + +#include +#include +#include + +using namespace DirectX::SimpleMath; +using AttachMode = VR::Settings::OverlayAttachMode; + +bool VR::ComputeWandIntersectionForOverlayType(OverlayType type, vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) +{ + float controllerM[3][4]; + if (!Util::GetControllerWorldMatrix(controllerIndex, controllerM)) { + return false; + } + Matrix controllerWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(controllerM)); + Vector3 rayOrigin = controllerWorld.Translation(); + Vector3 rayDir = controllerWorld.Forward(); + + // Update debug state + wandState.rayOrigin = rayOrigin; + wandState.rayDirection = rayDir; + Matrix overlayWorld; + if (type == OverlayType::HMD) { + if (settings.VRMenuPositioningMethod == 1) { // Fixed World + overlayWorld = fixedWorldOverlayPosition.m; + } else { // HMD Relative + vr::TrackedDevicePose_t hmdPose; + if (!Util::GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + return false; + if (!hmdPose.bPoseIsValid) + return false; + Matrix hmdWorld = Util::HmdMatrix34ToMatrix(hmdPose.mDeviceToAbsoluteTracking); + Matrix offset = Matrix::CreateTranslation(settings.VRMenuOffsetX, settings.VRMenuOffsetY, settings.VRMenuOffsetZ); + overlayWorld = offset * hmdWorld; + } + } else { // Controller Relative + vr::TrackedDeviceIndex_t attachIndex = Util::GetControllerIndexForDevice(settings.VRMenuAttachController, lastKnownLeftHandedMode); + if (attachIndex == vr::k_unTrackedDeviceIndexInvalid) + return false; + + float attachM[3][4]; + if (!Util::GetControllerWorldMatrix(attachIndex, attachM)) + return false; + Matrix attachWorld = Util::HmdMatrix34ToMatrix(Util::Float3x4ToHmdMatrix34(attachM)); + + Matrix offset = Matrix::CreateTranslation(settings.VRMenuControllerOffsetX, settings.VRMenuControllerOffsetY, settings.VRMenuControllerOffsetZ); + overlayWorld = offset * attachWorld; + } + + if (settings.VRMenuScale < 1e-4f) + return false; + overlayWorld = Config::CreateOverlayScaleMatrix(settings.VRMenuScale) * overlayWorld; + + Matrix worldToOverlay = overlayWorld.Invert(); + Vector3 localOrigin = Vector3::Transform(rayOrigin, worldToOverlay); + Vector3 localDir = Vector3::TransformNormal(rayDir, worldToOverlay); + + if (std::abs(localDir.z) < 1e-6f) + return false; + + float t = -localOrigin.z / localDir.z; + if (t < 0.0f) + return false; + + Vector3 hit = localOrigin + t * localDir; + + if (hit.x < -0.5f || hit.x > 0.5f || hit.y < -0.5f || hit.y > 0.5f) + return false; + + outUV.x = hit.x + 0.5f; + outUV.y = 0.5f - hit.y; + + return true; +} + +bool VR::ComputeWandIntersection(vr::TrackedDeviceIndex_t controllerIndex, ImVec2& outUV) +{ + bool intersected = false; + if (settings.attachMode == AttachMode::HMDOnly || settings.attachMode == AttachMode::Both) { + if (ComputeWandIntersectionForOverlayType(OverlayType::HMD, controllerIndex, outUV)) { + intersected = true; + } + } + if (!intersected && (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both)) { + if (ComputeWandIntersectionForOverlayType(OverlayType::Controller, controllerIndex, outUV)) { + intersected = true; + } + } + + if (intersected) { + wandState.isIntersecting = true; + wandState.uvCoordinates = outUV; + wandState.controllerIndex = controllerIndex; + } else { + wandState.isIntersecting = false; + } + + return intersected; +} + +void VR::UpdateCursorFromWandPointing() +{ + if (!settings.EnableWandPointing || !globals::menu || !globals::menu->IsEnabled) + return; + + ImGuiIO& io = ImGui::GetIO(); + + vr::TrackedDeviceIndex_t pointingController = vr::k_unTrackedDeviceIndexInvalid; + + if (settings.attachMode == AttachMode::ControllerOnly || settings.attachMode == AttachMode::Both) { + ControllerDevice oppositeController = (settings.VRMenuAttachController == ControllerDevice::Primary) ? + ControllerDevice::Secondary : + ControllerDevice::Primary; + pointingController = Util::GetControllerIndexForDevice(oppositeController, lastKnownLeftHandedMode); + } else { + pointingController = Util::GetControllerIndexForDevice(ControllerDevice::Primary, lastKnownLeftHandedMode); + } + + if (pointingController == vr::k_unTrackedDeviceIndexInvalid) { + wandState.isIntersecting = false; + return; + } + + ImVec2 uv; + bool intersected = ComputeWandIntersection(pointingController, uv); + + if (intersected) { + float screenX = uv.x * io.DisplaySize.x; + float screenY = uv.y * io.DisplaySize.y; + + screenX = std::clamp(screenX, 0.0f, io.DisplaySize.x); + screenY = std::clamp(screenY, 0.0f, io.DisplaySize.y); + + io.MousePos = ImVec2(screenX, screenY); + io.AddMousePosEvent(screenX, screenY); + io.MouseDrawCursor = true; + io.WantSetMousePos = true; + } else { + wandState.isIntersecting = false; + io.MouseDrawCursor = false; + io.WantSetMousePos = false; + } +} diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index 41621156dd..4f3c4b37b6 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -155,6 +155,8 @@ void VolumetricLighting::SaveSettings(json& o_json) void VolumetricLighting::RestoreDefaultSettings() { settings = {}; + if (globals::game::isVR) + Util::ResetGameSettingsToDefaults(hiddenVRSettings); } void VolumetricLighting::DataLoaded() @@ -208,8 +210,10 @@ void VolumetricLighting::SetupResources() void VolumetricLighting::EarlyPrepass() { - int32_t width = static_cast((float)globals::game::graphicsState->screenWidth); - int32_t height = static_cast((float)globals::game::graphicsState->screenHeight); + auto renderSize = Util::ConvertToDynamic(globals::state->screenSize); + + int32_t width = static_cast(renderSize.x); + int32_t height = static_cast(renderSize.y); if (width != vlData.screenX || height != vlData.screenY) { blurHCS = nullptr; @@ -237,11 +241,17 @@ void VolumetricLighting::EarlyPrepass() void VolumetricLighting::SetupVL() { if (inInterior) { - *bEnableVolumetricLighting = settings.InteriorEnabled && inInteriorWithSun; + if (globals::game::isVR) + SetBooleanSettings(hiddenVRSettings, GetName(), settings.InteriorEnabled && inInteriorWithSun); + else + *bEnableVolumetricLighting = settings.InteriorEnabled && inInteriorWithSun; *gVolumetricLightingSizeHigh = static_cast(settings.InteriorQuality) == Quality::Custom ? settings.InteriorCustomSize : defaultSizeHigh; SetVLQuality(GetVLDescriptor(), settings.InteriorQuality); } else { - *bEnableVolumetricLighting = settings.ExteriorEnabled; + if (globals::game::isVR) + SetBooleanSettings(hiddenVRSettings, GetName(), settings.ExteriorEnabled); + else + *bEnableVolumetricLighting = settings.ExteriorEnabled; *gVolumetricLightingSizeHigh = static_cast(settings.ExteriorQuality) == Quality::Custom ? settings.ExteriorCustomSize : defaultSizeHigh; SetVLQuality(GetVLDescriptor(), settings.ExteriorQuality); } @@ -261,6 +271,20 @@ void VolumetricLighting::SetVLQuality(VolumetricLightingDescriptor& descriptor, func(descriptor, std::clamp(quality, 0, 2)); } +void VolumetricLighting::RenderVolumetricLighting(VolumetricLightingDescriptor* descriptor, RE::NiCamera* camera, bool flag) +{ + using func_t = decltype(&VolumetricLighting::RenderVolumetricLighting); + static REL::Relocation func{ REL::RelocationID(100306, 0) }; + func(descriptor, camera, flag); +} + +void VolumetricLighting::RenderDepth::thunk() +{ + func(); + if (globals::features::volumetricLighting.bEnableVolumetricLighting) + RenderVolumetricLighting(&GetVLDescriptor(), RE::Main::WorldRootCamera(), false); +} + RE::BSImagespaceShader* VolumetricLighting::CreateShader(const std::string_view& name, const std::string_view& fileName, RE::BSComputeShader* computeShader) { auto shader = RE::BSImagespaceShader::Create(); diff --git a/src/Features/VolumetricLighting.h b/src/Features/VolumetricLighting.h index 553073ed34..9c058f2c8c 100644 --- a/src/Features/VolumetricLighting.h +++ b/src/Features/VolumetricLighting.h @@ -63,6 +63,24 @@ struct VolumetricLighting : Feature virtual void SetupResources() override; virtual void EarlyPrepass() override; + std::map hiddenVRSettings{ + { "bEnableVolumetricLighting:Display", { "Enable VL Shaders (INI) ", + "Enables volumetric lighting effects by creating shaders. " + "Needed at startup. ", + 0x1ed63d8, true, false, true } }, + { "bVolumetricLightingEnable:Display", { "Enable VL (INI))", "Enables volumetric lighting. ", 0x3485360, true, false, true } }, + { "bVolumetricLightingUpdateWeather:Display", { "Enable Volumetric Lighting (Weather) (INI) ", + "Enables volumetric lighting for weather. " + "Only used during startup and used to set bVLWeatherUpdate.", + 0x3485361, true, false, true } }, + { "bVLWeatherUpdate", { "Enable VL (Weather)", "Enables volumetric lighting for weather.", 0x3485363, true, false, true } }, + { "bVolumetricLightingEnabled_143232EF0", { "Enable VL (Papyrus) ", + "Enables volumetric lighting. " + "This is the Papyrus command. ", + REL::Relocate(0x3232ef0, 0, 0x3485362), true, false, true } }, + }; + + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; static RE::BSImagespaceShader* CreateShader(const std::string_view& name, const std::string_view& fileName, RE::BSComputeShader* computeShader); @@ -74,6 +92,20 @@ struct VolumetricLighting : Feature void SetGroupCountsHCS(uint32_t& threadGroupCountX) const; void SetGroupCountsVCS(uint32_t& threadGroupCountY) const; + // hooks + + struct CopyResource + { + static void thunk(ID3D11DeviceContext* a_this, ID3D11Resource* a_renderTarget, ID3D11Resource* a_renderTargetSource); + static inline REL::Relocation func; + }; + + struct RenderDepth + { + static void thunk(); + static inline REL::Relocation func; + }; + private: struct VolumetricLightingDescriptor {}; @@ -81,6 +113,8 @@ struct VolumetricLighting : Feature static const char* FromUnits(int32_t value, int32_t unitScale); static VolumetricLightingDescriptor& GetVLDescriptor(); static void SetVLQuality(VolumetricLightingDescriptor& descriptor, std::uint32_t quality); + static void RenderVolumetricLighting(VolumetricLightingDescriptor* descriptor, RE::NiCamera* camera, bool flag); + void DrawVolumetricLightingSettings(int32_t& quality, TextureSize& customSize, bool isInterior, bool inLocationType); TextureSize& FetchCurrentSizeInUnits(bool interior); void SetupVL(); diff --git a/src/Features/VolumetricShadows.cpp b/src/Features/VolumetricShadows.cpp index b59bf36c94..34459618ed 100644 --- a/src/Features/VolumetricShadows.cpp +++ b/src/Features/VolumetricShadows.cpp @@ -372,7 +372,7 @@ struct CreateDepthStencil_VolumetricLighting void VolumetricShadows::PostPostLoad() { - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x9DC, 0x9DC)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x9DC, 0x9DC, 0xC60)); } bool VolumetricShadows::HasShaderDefine(RE::BSShader::Type) diff --git a/src/Features/VolumetricShadows.h b/src/Features/VolumetricShadows.h index 0830395070..e7d5a385eb 100644 --- a/src/Features/VolumetricShadows.h +++ b/src/Features/VolumetricShadows.h @@ -64,6 +64,7 @@ struct VolumetricShadows : Feature virtual void SaveSettings(json& o_json) override; virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; diff --git a/src/Features/WaterEffects.h b/src/Features/WaterEffects.h index e64a04ea30..87a79d1f36 100644 --- a/src/Features/WaterEffects.h +++ b/src/Features/WaterEffects.h @@ -28,5 +28,6 @@ struct WaterEffects : Feature virtual void Prepass() override; + virtual bool SupportsVR() override { return true; }; virtual bool IsCore() const override { return true; }; }; diff --git a/src/Features/WetnessEffects.h b/src/Features/WetnessEffects.h index f7b31e4a96..ff4c28d4d2 100644 --- a/src/Features/WetnessEffects.h +++ b/src/Features/WetnessEffects.h @@ -118,6 +118,7 @@ struct WetnessEffects : Feature virtual void RestoreDefaultSettings() override; + virtual bool SupportsVR() override { return true; }; // Override to provide weather analysis configuration virtual WeatherAnalysisConfig GetWeatherAnalysisConfig() const override diff --git a/src/FrameAnnotations.cpp b/src/FrameAnnotations.cpp index 716c873195..5858b740f8 100644 --- a/src/FrameAnnotations.cpp +++ b/src/FrameAnnotations.cpp @@ -16,7 +16,8 @@ namespace FrameAnnotations if (globals::state && globals::state->IsDeveloperMode()) { uint16_t packed = static_cast(EffectType); uint16_t se = RE::ImageSpaceManager::GetSEIndex(EffectType); - std::string packedString = std::format(" (packed: 0x{:X}, SE: {})", packed, se); + uint16_t vr = RE::ImageSpaceManager::GetVRIndex(EffectType); + std::string packedString = std::format(" (packed: 0x{:X}, SE: {}, VR: {})", packed, se, vr); return enumName + packedString; } else { return enumName; @@ -219,6 +220,39 @@ namespace FrameAnnotations static inline REL::Relocation func; }; + struct VR_RenderDepth_DownscaleDepthBuffer + { + static void thunk(RE::BSSceneGraph* a1) + { + globals::state->BeginPerfEvent("DownscaleDepthBuffer"); + func(a1); + globals::state->EndPerfEvent(); + }; + static inline REL::Relocation func; + }; + + struct VR_RenderDepth_BSOBBOcclusionTestingShader + { + static void thunk(RE::BSImagespaceShader* a_this, RE::ImageSpaceEffectParam* a_param) + { + globals::state->BeginPerfEvent("BSOBBOcclusionTestingShader"); + func(a_this, a_param); + globals::state->EndPerfEvent(); + }; + static inline REL::Relocation func; + }; + + struct VR_UpscaleDepthBuffer + { + static void thunk(RE::ImageSpaceManager* a_this, unsigned int a2, RE::RENDER_TARGET a_target, RE::RENDER_TARGET a_target2, __int64 a5, bool a6) + { + globals::state->BeginPerfEvent("UpscaleDepthBuffer"); + func(a_this, a2, a_target, a_target2, a5, a6); + globals::state->EndPerfEvent(); + }; + static inline REL::Relocation func; + }; + struct Main_RenderWorld { static void thunk(bool a1) @@ -1023,7 +1057,7 @@ namespace FrameAnnotations auto renderer = globals::game::renderer; for (size_t renderTargetIndex = 0; - renderTargetIndex < RE::RENDER_TARGETS::kTOTAL; ++renderTargetIndex) { + renderTargetIndex < Util::GetRenderTargetCount(); ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast(renderTargetIndex)); if (auto texture = renderer->GetRuntimeData().renderTargets[renderTargetIndex].texture) { @@ -1044,7 +1078,7 @@ namespace FrameAnnotations } for (size_t renderTargetIndex = 0; - renderTargetIndex < RE::RENDER_TARGETS_DEPTHSTENCIL::kTOTAL; + renderTargetIndex < Util::GetDepthStencilCount(); ++renderTargetIndex) { const auto renderTargetName = magic_enum::enum_name( static_cast( diff --git a/src/Globals.cpp b/src/Globals.cpp index 04f4e38a01..aebf343baa 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -32,6 +32,7 @@ #include "Features/TerrainVariation.h" #include "Features/UnifiedWater.h" #include "Features/Upscaling.h" +#include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" @@ -78,6 +79,7 @@ namespace globals TerrainShadows terrainShadows{}; UnifiedWater unifiedWater{}; VolumetricLighting volumetricLighting{}; + VR vr{}; WaterEffects waterEffects{}; PerformanceOverlay performanceOverlay{}; WetnessEffects wetnessEffects{}; @@ -106,6 +108,7 @@ namespace globals RE::BSGraphics::Renderer* renderer = nullptr; RE::BSShaderManager::State* smState = nullptr; RE::TES* tes = nullptr; + bool isVR = false; RE::MemoryManager* memoryManager = nullptr; RE::INISettingCollection* iniSettingCollection = nullptr; RE::INIPrefSettingCollection* iniPrefSettingCollection = nullptr; @@ -192,9 +195,9 @@ namespace globals cameraFar = (float*)(REL::RelocationID(517032, 403540).address() + 0x44); deltaTime = (float*)REL::RelocationID(523660, 410199).address(); - currentPixelShader = &(shadowState->GetRuntimeData().currentPixelShader); - currentVertexShader = &(shadowState->GetRuntimeData().currentVertexShader); - stateUpdateFlags = &(shadowState->GetRuntimeData().stateUpdateFlags); + currentPixelShader = GET_INSTANCE_MEMBER_PTR(currentPixelShader, shadowState); + currentVertexShader = GET_INSTANCE_MEMBER_PTR(currentVertexShader, shadowState); + stateUpdateFlags = GET_INSTANCE_MEMBER_PTR(stateUpdateFlags, shadowState); ui = RE::UI::GetSingleton(); calendar = RE::Calendar::GetSingleton(); @@ -303,6 +306,107 @@ namespace globals static inline REL::Relocation func; }; + /** + * @brief Hooked OMSetRenderTargets — injects POM offset UAV at slot 7 when in the deferred pass. + * + * vtable index 33 for ID3D11DeviceContext::OMSetRenderTargets. + * After Skyrim binds the deferred MRT (clearing all UAVs), this hook re-adds the POM offset + * UAV at slot u7 so the Lighting PS (VR_STEREO_OPT permutation) can write per-pixel parallax + * depth offsets without overloading Reflectance.w. + */ + struct ID3D11DeviceContext_OMSetRenderTargets + { + static void STDMETHODCALLTYPE thunk(ID3D11DeviceContext* This, UINT NumViews, ID3D11RenderTargetView* const* ppRenderTargetViews, ID3D11DepthStencilView* pDepthStencilView) + { + func(This, NumViews, ppRenderTargetViews, pDepthStencilView); + + // D3D11 handles any SRV/UAV conflict automatically (silently unbinds the UAV when + // the same resource is later bound as an SRV), so no NumViews guard is needed. + if (globals::deferred->deferredPass) { + auto& stereoOpt = globals::features::vr.stereoOpt; + if (stereoOpt.loaded) { + if (auto* uav = stereoOpt.GetPomOffsetUAV()) { + This->OMSetRenderTargetsAndUnorderedAccessViews( + D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL, nullptr, nullptr, + 7, 1, &uav, nullptr); + } + } + } + } + static inline REL::Relocation func; + }; + + /** + * @brief Hooked OMSetDepthStencilState — replaces DSS with stencil-enforcing version when VR stereo opt is active. + * + * vtable index 36 for ID3D11DeviceContext::OMSetDepthStencilState. + * When VRStereoOptimizations has written stencil marks, this hook transparently swaps + * the game's DSS for a modified version that adds a stencil NOT_EQUAL test, causing + * marked Eye 1 pixels to be skipped during normal rendering. + */ + struct ID3D11DeviceContext_OMSetDepthStencilState + { + static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilState* pDepthStencilState, UINT StencilRef) + { + if (globals::game::isVR) { + auto& stereoOpt = globals::features::vr.stereoOpt; + if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { + pDepthStencilState = stereoOpt.GetOrCreateModifiedDSS(pDepthStencilState); + stereoOpt.NoteStencilSwap(); + StencilRef = 1; // Must match the ref written by our stencil pass + } + } + func(This, pDepthStencilState, StencilRef); + } + static inline REL::Relocation func; + }; + + /** + * @brief Hooked ClearDepthStencilView — blocks stencil clears when VR stereo opt stencil is active. + * + * vtable index 53 for ID3D11DeviceContext::ClearDepthStencilView. + * Prevents the game from clearing our stencil marks between the stencil write and + * the stereo overwrite blend pass by stripping the D3D11_CLEAR_STENCIL flag. + */ + struct ID3D11DeviceContext_ClearDepthStencilView + { + static void thunk(ID3D11DeviceContext* This, ID3D11DepthStencilView* pDepthStencilView, UINT ClearFlags, FLOAT Depth, UINT8 Stencil) + { + if (globals::game::isVR) { + auto& stereoOpt = globals::features::vr.stereoOpt; + if (stereoOpt.loaded && stereoOpt.IsStencilActive()) { + // Only protect the main scene DSV — allow other DSVs to clear normally + auto renderer = globals::game::renderer; + auto& mainDepth = renderer->GetDepthStencilData().depthStencils[RE::RENDER_TARGETS_DEPTHSTENCIL::kMAIN]; + if (mainDepth.views[0]) { + // Compare the DSV being cleared against the main scene DSV + ID3D11Resource* clearRes = nullptr; + ID3D11Resource* mainRes = nullptr; + pDepthStencilView->GetResource(&clearRes); + mainDepth.views[0]->GetResource(&mainRes); + bool isMainDSV = (clearRes == mainRes); + if (clearRes) + clearRes->Release(); + if (mainRes) + mainRes->Release(); + if (isMainDSV) { + ClearFlags &= ~D3D11_CLEAR_STENCIL; + if (ClearFlags == 0) + return; + } + } + } + } + func(This, pDepthStencilView, ClearFlags, Depth, Stencil); + } + static inline REL::Relocation func; + }; + + /** + * @brief Installs hooks on the Map and Unmap methods of the provided D3D11 device context. + * + * This enables interception of resource mapping and unmapping operations for frame buffer caching. + */ void InstallD3DHooks(ID3D11DeviceContext* a_context) { stl::detour_vfunc<14, ID3D11DeviceContext_Map>(a_context); diff --git a/src/Globals.h b/src/Globals.h index a3e640ebaa..bce0ab0b48 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -26,6 +26,7 @@ struct TerrainHelper; struct TerrainShadows; struct UnifiedWater; struct VolumetricLighting; +struct VR; struct WaterEffects; struct PerformanceOverlay; struct WetnessEffects; @@ -86,6 +87,7 @@ namespace globals extern TerrainShadows terrainShadows; extern UnifiedWater unifiedWater; extern VolumetricLighting volumetricLighting; + extern VR vr; extern WaterEffects waterEffects; extern PerformanceOverlay performanceOverlay; extern WetnessEffects wetnessEffects; @@ -222,6 +224,7 @@ namespace globals extern RE::BSGraphics::Renderer* renderer; extern RE::BSShaderManager::State* smState; extern RE::TES* tes; + extern bool isVR; extern RE::MemoryManager* memoryManager; extern RE::INISettingCollection* iniSettingCollection; extern RE::INIPrefSettingCollection* iniPrefSettingCollection; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index e72d0bc0a7..0c425e4eab 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -390,7 +390,7 @@ struct BSShaderRenderTargets_Create */ static inline Util::GameSetting iNumFocusShadow{ "Number of Focus Shadows (INI)", "Controls the number of focus shadows.", - static_cast(0), 4, 0, 4 }; + REL::Relocate(0, 0, 0x1ed6368), 4, 0, 4 }; static void thunk() { @@ -448,8 +448,33 @@ struct BSInputDeviceManager_PollInputDevices if (*a_events) { if (auto device = (*a_events)->GetDevice()) { + if (globals::game::isVR) { + // In VR, block mouse/keyboard input when menu is open (like Flatrim) + // Allow gamepad input to pass through + // Also handle VR controller devices based on OpenVR compatibility + bool isVRController = ((device == RE::INPUT_DEVICES::INPUT_DEVICE::kVivePrimary) || + (device == RE::INPUT_DEVICES::INPUT_DEVICE::kViveSecondary) || + (device == RE::INPUT_DEVICES::INPUT_DEVICE::kOculusPrimary) || + (device == RE::INPUT_DEVICES::INPUT_DEVICE::kOculusSecondary) || + (device == RE::INPUT_DEVICES::INPUT_DEVICE::kWMRPrimary) || + (device == RE::INPUT_DEVICES::INPUT_DEVICE::kWMRSecondary)); + + // Allow gamepad input to pass through always + if (device == RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad) { + blockedDevice = false; + } + // For VR controllers, only block if OpenVR is compatible + else if (isVRController) { + blockedDevice = globals::features::vr.IsOpenVRCompatible(); + } + // For mouse/keyboard and other devices, block them (like Flatrim) + else { + blockedDevice = true; + } + } else { // Block all devices except gamepad when menu is open blockedDevice = (device != RE::INPUT_DEVICES::INPUT_DEVICE::kGamepad); + } } } } @@ -996,7 +1021,7 @@ namespace Hooks // This input hook also drives per-frame Reflex update (see BSInputDeviceManager_PollInputDevices::thunk). logger::info("Hooking BSInputDeviceManager::PollInputDevices"); - stl::write_thunk_call(REL::RelocationID(67315, 68617).address() + REL::Relocate(0x7B, 0x7B)); + stl::write_thunk_call(REL::RelocationID(67315, 68617).address() + REL::Relocate(0x7B, 0x7B, 0x81)); logger::info("Hooking BSShader::LoadShaders"); stl::detour_thunk(REL::RelocationID(101339, 108326)); @@ -1027,8 +1052,8 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x46B, 0x46E, 0x5C3)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x4F0, 0x4EF, 0x64E)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502, 0x661)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19, 0xE06)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF4F, 0xF51)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF65, 0xF67)); @@ -1054,7 +1079,7 @@ namespace Hooks stl::detour_thunk(REL::RelocationID(75532, 77329)); logger::info("Hooking TESWaterReflections::Update_Actor::GetLOSPosition for Sky Reflection Fix"); - stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA)); + stl::write_thunk_call(REL::RelocationID(31373, 32160).address() + REL::Relocate(0x1AD, 0x1CA, 0x1ed)); logger::info("Hooking Sky::UpdateColors"); stl::detour_thunk(REL::RelocationID(25686, 26233)); diff --git a/src/Menu.cpp b/src/Menu.cpp index 6beccdf2c5..2fa61aeb7b 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -47,6 +47,7 @@ #include "Features/PerformanceOverlay/ABTesting/ABTestAggregator.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/ScreenshotFeature.h" +#include "Features/VR.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -936,7 +937,7 @@ void Menu::DrawFooter() * callbacks for input processing, settings rendering, and key mapping. This method * serves as the bridge between Menu's state and the extracted overlay rendering logic. * - * Handles input event processing, shader compilation status, feature overlays, + * Handles VR setup, input event processing, shader compilation status, feature overlays, * A/B testing, and ImGui frame management through the specialized renderer component. */ void Menu::DrawOverlay() @@ -992,10 +993,11 @@ void Menu::DrawOverlay() } /** - * @brief Processes queued input events + * @brief Processes queued input events for both VR and non-VR devices * - * This method handles the logic of routing input events to appropriate handlers: - * - Keyboard and mouse events are processed directly for ImGui integration + * This method handles the complex logic of routing input events to appropriate handlers: + * - VR controller events are forwarded to the VR system for specialized processing + * - Non-VR events (keyboard, mouse) are processed directly for ImGui integration * - Includes key state normalization and stuck key detection/correction * * The method maintains thread safety through mutex protection of the input event queue. @@ -1046,7 +1048,28 @@ void Menu::ProcessInputEventQueue() std::unique_lock mutex(_inputEventMutex); ImGuiIO& io = ImGui::GetIO(); + // Split the queue into VR and non-VR events + std::vector vrEvents; + std::vector nonVREvents; for (auto& event : _keyEventQueue) { + bool isVRController = ((event.device == RE::INPUT_DEVICE::kVivePrimary || event.device == RE::INPUT_DEVICE::kViveSecondary || + event.device == RE::INPUT_DEVICE::kOculusPrimary || event.device == RE::INPUT_DEVICE::kOculusSecondary || + event.device == RE::INPUT_DEVICE::kWMRPrimary || event.device == RE::INPUT_DEVICE::kWMRSecondary)); + + if (globals::features::vr.IsOpenVRCompatible() && isVRController) { + vrEvents.push_back(event); + } else { + nonVREvents.push_back(event); + } + } + // Process VR events in VR + if (!vrEvents.empty()) { + globals::features::vr.ProcessVREvents(vrEvents); + globals::features::vr.UpdateOverlayMenuStateFromInput(); + } + + // Process non-VR events in Menu + for (auto& event : nonVREvents) { if (event.eventType == RE::INPUT_EVENT_TYPE::kChar) { io.AddInputCharacter(event.keyCode); continue; diff --git a/src/Menu.h b/src/Menu.h index 4ffccecdb3..e610271f31 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -546,6 +546,8 @@ class Menu [[nodiscard]] constexpr bool IsHeld() const noexcept { return IsPressed() && IsRepeating(); } [[nodiscard]] constexpr bool IsUp() const noexcept { return (value == 0.0F) && IsRepeating(); } }; + // VR overlay input and cursor helpers + void ProcessVROverlayInput(); private: Settings settings; diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 8e4b232a73..5a5a45b418 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -24,6 +24,8 @@ #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" +#include "Features/VR.h" + namespace { std::unordered_map s_windowOverlapAlpha; @@ -135,8 +137,13 @@ void OverlayRenderer::RenderOverlay( float& cachedFontSize, float currentFontSize) { + HandleVRSetup(); processInputEventQueue(); + if (globals::features::vr.IsOpenVRCompatible()) { + globals::features::vr.ProcessControllerInputForImGui(); + } + if (ShouldSkipRendering()) { auto& io = ImGui::GetIO(); io.ClearInputKeys(); @@ -181,6 +188,13 @@ void OverlayRenderer::RenderOverlay( FinalizeImGuiFrame(); } +void OverlayRenderer::HandleVRSetup() +{ + if (globals::features::vr.IsOpenVRCompatible()) { + globals::features::vr.RecreateOverlayTexturesIfNeeded(); + } +} + bool OverlayRenderer::ShouldSkipRendering() { auto shaderCache = globals::shaderCache; @@ -381,6 +395,9 @@ void OverlayRenderer::FinalizeImGuiFrame() ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + if (globals::features::vr.IsOpenVRCompatible()) { + globals::features::vr.SubmitOverlayFrame(); + } } void OverlayRenderer::RenderFirstTimeSetupOverlay() diff --git a/src/Menu/OverlayRenderer.h b/src/Menu/OverlayRenderer.h index b9b9dc6720..d78e908319 100644 --- a/src/Menu/OverlayRenderer.h +++ b/src/Menu/OverlayRenderer.h @@ -10,7 +10,7 @@ class Menu; * @brief Specialized renderer component for overlay and frame management * * This class was extracted from Menu.cpp to handle all overlay-related rendering - * responsibilities including shader compilation status, feature overlays, + * responsibilities including VR setup, shader compilation status, feature overlays, * A/B testing, and ImGui frame lifecycle management. * * The renderer uses a callback-based architecture to maintain separation of concerns @@ -25,7 +25,7 @@ class OverlayRenderer /** * @brief Main overlay rendering entry point * - * Coordinates all overlay rendering activities including input processing, + * Coordinates all overlay rendering activities including VR setup, input processing, * shader compilation status display, feature overlays, A/B testing, and ImGui frame * management. Uses callback functions to access Menu functionality while maintaining * architectural separation. @@ -46,6 +46,7 @@ class OverlayRenderer float currentFontSize); private: + static void HandleVRSetup(); static bool ShouldSkipRendering(); static void HandleFontReload(Menu& menu, float& cachedFontSize, float currentFontSize); static void InitializeImGuiFrame(Menu& menu); diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index e1e4ddb791..c20cff5cd2 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -7,6 +7,7 @@ #include "BackgroundBlur.h" #include "Features/ScreenshotFeature.h" +#include "Features/VR.h" #include "Fonts.h" #include "Globals.h" #include "I18n/I18n.h" diff --git a/src/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 87fb28acfd..b43784f8c6 100644 --- a/src/Menu/ThemeManager.cpp +++ b/src/Menu/ThemeManager.cpp @@ -28,6 +28,8 @@ #include "../Util.h" #include "../Utils/FileSystem.h" #include "../Utils/UI.h" +#include "Features/VR.h" + using namespace SKSE; namespace @@ -1018,9 +1020,12 @@ float ThemeManager::ResolveFontSize(const Menu& menu) // Compute dynamic size from screen resolution float dynamicSize; - if (globals::game::graphicsState && globals::game::graphicsState->screenHeight > 0) { - // Use current screen height - dynamicSize = (float)globals::game::graphicsState->screenHeight * Constants::DEFAULT_FONT_RATIO; + if (globals::game::isVR) { + // VR: use overlay height + dynamicSize = VR::Config::kOverlayHeight * Constants::DEFAULT_FONT_RATIO; + } else if (globals::state && globals::state->screenSize.y > 0) { + // Non-VR: use current screen height + dynamicSize = globals::state->screenSize.y * Constants::DEFAULT_FONT_RATIO; } else { // Fallback: use default font size logger::warn("ThemeManager::ResolveFontSize() - Falling back to Constants::DEFAULT_FONT_SIZE due to missing screen height."); diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index a8e3fc0064..feb711bf8c 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -789,12 +789,12 @@ namespace SIE { "SplitDistance", lightingPSConstants.SplitDistance }, { "SSRParams", lightingPSConstants.SSRParams }, { "WorldMapOverlayParametersPS", lightingPSConstants.WorldMapOverlayParametersPS }, - { "ShadowSampleParam", lightingPSConstants.ShadowSampleParam }, - { "EndSplitDistances", lightingPSConstants.EndSplitDistances }, - { "StartSplitDistances", lightingPSConstants.StartSplitDistances }, - { "DephBiasParam", lightingPSConstants.DephBiasParam }, - { "ShadowLightParam", lightingPSConstants.ShadowLightParam }, - { "ShadowMapProj", lightingPSConstants.ShadowMapProj }, + { "ShadowSampleParam", lightingPSConstants.ShadowSampleParam }, // VR only + { "EndSplitDistances", lightingPSConstants.EndSplitDistances }, // VR only + { "StartSplitDistances", lightingPSConstants.StartSplitDistances }, // VR only + { "DephBiasParam", lightingPSConstants.DephBiasParam }, // VR only + { "ShadowLightParam", lightingPSConstants.ShadowLightParam }, // VR only + { "ShadowMapProj", lightingPSConstants.ShadowMapProj }, // VR only { "AmbientColor", lightingPSConstants.AmbientColor }, { "FogColor", lightingPSConstants.FogColor }, { "ColourOutputClamp", lightingPSConstants.ColourOutputClamp }, @@ -813,8 +813,8 @@ namespace SIE { "LandscapeTexture5to6IsSpecPower", lightingPSConstants.LandscapeTexture5to6IsSpecPower }, { "SnowRimLightParameters", lightingPSConstants.SnowRimLightParameters }, { "CharacterLightParams", lightingPSConstants.CharacterLightParams }, - { "InvWorldMat", lightingPSConstants.InvWorldMat }, - { "PreviousWorldMat", lightingPSConstants.PreviousWorldMat }, + { "InvWorldMat", lightingPSConstants.InvWorldMat }, // VR only + { "PreviousWorldMat", lightingPSConstants.PreviousWorldMat }, // VR only { "PBRFlags", lightingPSConstants.PBRFlags }, { "PBRParams1", lightingPSConstants.PBRParams1 }, @@ -1850,6 +1850,7 @@ namespace SIE // BSImagespaceShaderGraphicsTextureFilterMode is intentionally omitted because VR index 111 is ISReflectionBlurHCS. { "BSImagespaceShaderISDownsampleHierarchicalDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDownsampleHierarchicalDepthBufferCS) }, { "BSImagespaceShaderISDiffScaleDownsampleDepthBufferCS", RE::ImageSpaceManager::GetCurrentIndex(ISDiffScaleDownsampleDepthBufferCS) }, + { "BSImagespaceShaderISFullScreenVR", RE::ImageSpaceManager::GetCurrentIndex(ISFullScreenVR) }, { "BSImagespaceShaderISTransformLvl7PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISTransformLvl7PreTest) }, { "BSImagespaceShaderISLvl6PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl6PreTest) }, { "BSImagespaceShaderISLvl5PreTest", RE::ImageSpaceManager::GetCurrentIndex(ISLvl5PreTest) }, @@ -1887,6 +1888,9 @@ namespace SIE } auto state = globals::state; + if (globals::game::isVR && strcmp(shader.fxpFilename, "OBBOcclusionTesting") == 0) + // use vanilla shader + return nullptr; if (!((ShaderCache::IsSupportedShader(shader) || state->IsDeveloperMode() && state->IsShaderEnabled(shader)) && state->enableVShaders)) { return nullptr; @@ -1928,6 +1932,9 @@ namespace SIE uint32_t descriptor) { auto state = globals::state; + if (globals::game::isVR && strcmp(shader.fxpFilename, "OBBOcclusionTesting") == 0) + // use vanilla shader + return nullptr; if (!((ShaderCache::IsSupportedShader(shader) || state->IsDeveloperMode() && state->IsShaderEnabled(shader)) && state->enablePShaders)) { return nullptr; diff --git a/src/ShaderCache.h b/src/ShaderCache.h index 8d426e6dd6..0afa732404 100644 --- a/src/ShaderCache.h +++ b/src/ShaderCache.h @@ -139,10 +139,20 @@ namespace ShaderConstants { static const GrassPS& Get() { - static GrassPS instance{}; + static GrassPS instance = REL::Module::IsVR() ? GetVR() : GetFlat(); return instance; } + static GrassPS GetFlat() + { + return GrassPS{}; + } + + static GrassPS GetVR() + { + return GrassPS{}; + } + const int32_t PBRFlags = 0; const int32_t PBRParams1 = 1; const int32_t PBRParams2 = 2; @@ -152,10 +162,20 @@ namespace ShaderConstants { static const EffectPS& Get() { - static EffectPS instance{}; + static EffectPS instance = REL::Module::IsVR() ? GetVR() : GetFlat(); return instance; } + static EffectPS GetFlat() + { + return EffectPS{}; + } + + static EffectPS GetVR() + { + return EffectPS{}; + } + const int32_t PropertyColor = 0; const int32_t AlphaTestRef = 1; const int32_t MembraneRimColor = 2; @@ -356,6 +376,17 @@ namespace SIE inline static bool IsSupportedShader(const RE::BSShader::Type type) { + if (!REL::Module::IsVR()) + return type == RE::BSShader::Type::Lighting || + type == RE::BSShader::Type::BloodSplatter || + type == RE::BSShader::Type::DistantTree || + type == RE::BSShader::Type::Sky || + type == RE::BSShader::Type::Grass || + type == RE::BSShader::Type::Particle || + type == RE::BSShader::Type::Water || + type == RE::BSShader::Type::Effect || + type == RE::BSShader::Type::Utility || + type == RE::BSShader::Type::ImageSpace; return type == RE::BSShader::Type::Lighting || type == RE::BSShader::Type::BloodSplatter || type == RE::BSShader::Type::DistantTree || diff --git a/src/State.cpp b/src/State.cpp index 03636c083a..8dadef82be 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -206,7 +206,7 @@ void State::Reset() frameCountAtomic.store(frameCount, std::memory_order_relaxed); if (auto* imageSpaceManager = RE::ImageSpaceManager::GetSingleton()) { - auto& BSImagespaceShaderApplyReflections = imageSpaceManager->GetRuntimeData().BSImagespaceShaderApplyReflections; + GET_INSTANCE_MEMBER(BSImagespaceShaderApplyReflections, imageSpaceManager); // Disable reflections being applied to things other than water if (BSImagespaceShaderApplyReflections.get()) { @@ -215,7 +215,9 @@ void State::Reset() } // Disable "improved" snow shader, unsupported - RE::GetINISetting("bEnableImprovedSnow:Display")->data.b = false; + if (!globals::game::isVR) { + RE::GetINISetting("bEnableImprovedSnow:Display")->data.b = false; + } activeReflections = false; } @@ -728,6 +730,7 @@ void State::CheckTypedUAVLoadSupport() { DXGI_FORMAT_R16G16B16A16_FLOAT, "R16G16B16A16_FLOAT", "Dynamic Cubemaps (HDR), Skylighting outProbeArray" }, { DXGI_FORMAT_R16G16B16A16_UNORM, "R16G16B16A16_UNORM", "Grass Collision (collisionTexture)" }, { DXGI_FORMAT_R16G16_UNORM, "R16G16_UNORM", "Terrain Shadows (RWTexShadowHeights)" }, + { DXGI_FORMAT_R16G16_FLOAT, "R16G16_FLOAT", "VR Stereo Blend (kMOTION_VECTOR reprojection)" }, { DXGI_FORMAT_R8G8B8A8_UNORM, "R8G8B8A8_UNORM", "HDR Display UI brightness (uiTexture)" }, { DXGI_FORMAT_R8_UINT, "R8_UINT", "Skylighting accumulation frames (outAccumFramesArray)" }, { DXGI_FORMAT_R16_FLOAT, "R16_FLOAT", "Vanilla volumetric lighting density (DensityRW)" }, @@ -757,7 +760,7 @@ void State::CheckTypedUAVLoadSupport() logger::warn( "[TypedUAVLoad] One or more required formats lack typed-UAV-load support on this GPU. " "Affected features will read undefined data and may produce visual artifacts. " - "Consider disabling: Dynamic Cubemaps, Grass Collision, Terrain Shadows, Skylighting, HDR Display."); + "Consider disabling: Dynamic Cubemaps, Grass Collision, Terrain Shadows, Skylighting, HDR Display, VR Stereo Optimisations."); } } @@ -784,9 +787,11 @@ void State::SetupResources() featureDataCB = new ConstantBuffer(ConstantBufferDesc((uint32_t)size)); // Grab main texture to get resolution + // VR cannot use viewport->screenWidth/Height as it's the desktop preview window's resolution and not HMD D3D11_TEXTURE2D_DESC texDesc{}; renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kMAIN].texture->GetDesc(&texDesc); + screenSize = { (float)texDesc.Width, (float)texDesc.Height }; globals::d3d::context->QueryInterface(__uuidof(pPerf), reinterpret_cast(&pPerf)); featureLevel = globals::d3d::device->GetFeatureLevel(); @@ -970,14 +975,14 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b data.DirLightColor *= lightRuntimeData.fade; auto imageSpaceManager = RE::ImageSpaceManager::GetSingleton(); - data.DirLightColor *= imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale; + data.DirLightColor *= !globals::game::isVR ? imageSpaceManager->GetRuntimeData().data.baseData.hdr.sunlightScale : imageSpaceManager->GetVRRuntimeData().data.baseData.hdr.sunlightScale; const auto& direction = dirLight->GetWorldDirection(); data.DirLightDirection = { -direction.x, -direction.y, -direction.z, 0.0f }; data.DirLightDirection.Normalize(); data.CameraData = Util::GetCameraData(); - data.BufferDim = { (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight, 1.0f / (float)globals::game::graphicsState->screenWidth, 1.0f / (float)globals::game::graphicsState->screenHeight }; + data.BufferDim = { screenSize.x, screenSize.y, 1.0f / screenSize.x, 1.0f / screenSize.y }; data.Timer = timer; auto temporal = Util::GetTemporal(); @@ -994,7 +999,23 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b } } + // Fallback water height for the VR analytical mask when tile 12 returns the sentinel. + // Uses player->GetWaterHeight() (reads relevantWaterHeight from LOADED_REF_DATA) gated by + // underwaterCount > 0 so it is only set when the player is actually in a water body. + // Covers both interior water (where TES::GetWaterHeight returns -NI_INFINITY) and exterior + // partial submersion. Stored as eye-0 camera-relative Z to match WaterData[].w. data.WaterSystemHeight = -RE::NI_INFINITY; + if (globals::game::isVR) { + if (auto player = globals::game::player) { + if (player->loadedData && player->loadedData->underwaterCount > 0) { + float worldHeight = player->GetWaterHeight(); + if (worldHeight > -RE::NI_INFINITY) { + auto eye0Pos = Util::GetEyePosition(0); + data.WaterSystemHeight = worldHeight - eye0Pos.z; + } + } + } + } data.InInterior = Util::IsInterior(); data.HasDirectionalShadows = HasDirectionalShadows(); @@ -1011,9 +1032,8 @@ void State::UpdateSharedData([[maybe_unused]] bool a_inWorld, [[maybe_unused]] b if (upscaling.loaded) { auto upscaleMethod = upscaling.GetUpscaleMethod(); if (temporal && upscaleMethod != Upscaling::UpscaleMethod::kTAA) { - float2 screenSz{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; - auto renderSize = Util::ConvertToDynamic(screenSz, true); - data.MipBias = std::log2f(renderSize.x / screenSz.x); + auto renderSize = Util::ConvertToDynamic(screenSize, true); + data.MipBias = std::log2f(renderSize.x / screenSize.x); if (upscaleMethod == Upscaling::UpscaleMethod::kDLSS) data.MipBias -= 1.0f; } else { diff --git a/src/State.h b/src/State.h index 6aac4e4597..84e50a6f1b 100644 --- a/src/State.h +++ b/src/State.h @@ -257,7 +257,7 @@ class State uint InMapMenu; uint HideSky; float MipBias; - float WaterSystemHeight; // TES::GetWaterHeight in camera-relative Z; -NI_INFINITY when no water body found + float WaterSystemHeight; // TES::GetWaterHeight at eye-0 in camera-relative Z; -NI_INFINITY when no water body found (VR only) float3 pad0; float4 AmbientSHR; float4 AmbientSHG; @@ -287,6 +287,7 @@ class State std::atomic frameCountAtomic{ 0 }; // Skyrim constants + float2 screenSize = {}; D3D_FEATURE_LEVEL featureLevel; TracyD3D11Ctx tracyCtx = nullptr; // Tracy context diff --git a/src/TruePBR.h b/src/TruePBR.h index befc8db420..044baea62b 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -19,6 +19,7 @@ struct TruePBR : Feature virtual std::string GetShortName() override { return "TruePBR"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kMaterials; } virtual bool IsCore() const override { return true; } + virtual bool SupportsVR() override { return true; } virtual bool IsInMenu() const override { return true; } virtual bool DrawFailLoadMessage() const override { return false; } diff --git a/src/Utils/D3D.cpp b/src/Utils/D3D.cpp index e61ae105b3..897ccee8fb 100644 --- a/src/Utils/D3D.cpp +++ b/src/Utils/D3D.cpp @@ -30,7 +30,7 @@ namespace Util { if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return rt.SRV; @@ -45,7 +45,7 @@ namespace Util { if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return rt.RTV; @@ -62,7 +62,7 @@ namespace Util if (a_srv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_srv == rt.SRV || a_srv == rt.SRVCopy) { return std::string(magic_enum::enum_name(static_cast(i))); @@ -78,7 +78,7 @@ namespace Util using RENDER_TARGET = RE::RENDER_TARGETS::RENDER_TARGET; if (a_rtv) { if (auto r = globals::game::renderer) { - for (int i = 0; i < RE::RENDER_TARGETS::kTOTAL; i++) { + for (int i = 0; i < GetRenderTargetCount(); i++) { auto rt = r->GetRuntimeData().renderTargets[i]; if (a_rtv == rt.RTV) { return std::string(magic_enum::enum_name(static_cast(i))); diff --git a/src/Utils/Game.cpp b/src/Utils/Game.cpp index 3d78474029..f9f7872779 100644 --- a/src/Utils/Game.cpp +++ b/src/Utils/Game.cpp @@ -32,7 +32,7 @@ namespace Util { if (globals::game::shadowState) { if (auto tes = RE::TES::GetSingleton()) { - auto position = GetEyePosition(); + auto position = GetEyePosition(0); position.x += offsetX; position.y += offsetY; if (auto cell = tes->GetCell(position)) { @@ -93,7 +93,7 @@ namespace Util return float4(1.0f, 1.0f, 1.0f, -FLT_MAX); } - RE::NiPoint3 GetEyePosition() + RE::NiPoint3 GetAverageEyePosition() { auto shadowState = globals::game::shadowState; if (!globals::game::isVR) @@ -143,7 +143,7 @@ namespace Util static float& cameraFOVDeg = (*(float*)(REL::RelocationID(513786, 388785).address())); // FOV degrees float hFOVRad = cameraFOVDeg * (3.14159265359f / 180.0f); float unitHalfWidth = tan(hFOVRad / 2); // This is same as camera frustum RL - float unitHalfHeight = unitHalfWidth / ((float)globals::game::graphicsState->screenWidth / (float)globals::game::graphicsState->screenHeight); // frustum TB + float unitHalfHeight = unitHalfWidth / (globals::state->screenSize.x / globals::state->screenSize.y); // frustum TB float vFOVRad = 2.0f * atan(unitHalfHeight); return vFOVRad; } @@ -163,7 +163,7 @@ namespace Util DispatchCount GetScreenDispatchCount(bool a_dynamic) { - float2 resolution{ (float)globals::game::graphicsState->screenWidth, (float)globals::game::graphicsState->screenHeight }; + float2 resolution = globals::state->screenSize; if (a_dynamic) ConvertToDynamic(resolution); diff --git a/src/Utils/Game.h b/src/Utils/Game.h index 5ac9a46974..130d9d3b60 100644 --- a/src/Utils/Game.h +++ b/src/Utils/Game.h @@ -39,7 +39,9 @@ namespace Util bool GetTemporal(); float GetVerticalFOVRad(); - RE::NiPoint3 GetEyePosition(); + RE::NiPoint3 GetAverageEyePosition(); + RE::NiPoint3 GetEyePosition(int eyeIndex); + RE::BSGraphics::ViewData GetCameraData(int eyeIndex); float2 ConvertToDynamic(float2 a_size, bool a_ignoreLock = false); diff --git a/src/Utils/GameSetting.h b/src/Utils/GameSetting.h index b212c9ebbc..dff7d2f3fe 100644 --- a/src/Utils/GameSetting.h +++ b/src/Utils/GameSetting.h @@ -107,7 +107,7 @@ namespace Util * @brief Gets the value of a game setting, transparently handling INI-based and offset-based settings. * * For INI-based settings (offset == 0), tries INISettingCollection, INIPrefSettingCollection, - * then GameSettingCollection. For offset-based settings, + * then GameSettingCollection. For offset-based settings (e.g., VR where INI entries don't exist), * reads directly from the memory address. * * @tparam T The expected value type (bool, float, std::int32_t, or std::uint32_t). @@ -152,7 +152,7 @@ namespace Util * @brief Sets the value of a game setting, transparently handling INI-based and offset-based settings. * * For INI-based settings (offset == 0), tries INISettingCollection, INIPrefSettingCollection, - * then GameSettingCollection. For offset-based settings, + * then GameSettingCollection. For offset-based settings (e.g., VR where INI entries don't exist), * writes directly to the memory address. * * @tparam T The value type (bool, float, std::int32_t, or std::uint32_t). diff --git a/src/Utils/Input.h b/src/Utils/Input.h index 7e5ba6d768..d4d01aff61 100644 --- a/src/Utils/Input.h +++ b/src/Utils/Input.h @@ -7,15 +7,17 @@ /** * @brief Identifies the type of input device for input mapping + * + * Used to distinguish between VR controllers, keyboard, mouse, and gamepads. */ enum class InputDeviceType { - Primary = 0, - Secondary = 1, - Both = 2, + Primary = 0, ///< VR: The dominant hand controller (right for right-handed, left for left-handed) + Secondary = 1, ///< VR: The non-dominant hand controller + Both = 2, ///< VR: Both controllers simultaneously Keyboard = 3, ///< Keyboard input Mouse = 4, ///< Mouse input - Gamepad = 5 ///< Gamepad/Controller input + Gamepad = 5 ///< Gamepad/Controller input (non-VR) }; /** @@ -61,9 +63,9 @@ constexpr bool IsValidDevice(InputDeviceType device) * The upper 16 bits store the device type, lower 16 bits store the key code. * * Can represent: + * - VR Controller button presses * - Keyboard key presses * - Mouse button clicks - * - Gamepad button presses */ struct InputCombo { @@ -81,7 +83,12 @@ struct InputCombo { } - // Helper methods + // VR helper methods + static InputCombo Primary(uint32_t key) { return InputCombo(InputDeviceType::Primary, key); } + static InputCombo Secondary(uint32_t key) { return InputCombo(InputDeviceType::Secondary, key); } + static InputCombo Both(uint32_t key) { return InputCombo(InputDeviceType::Both, key); } + + // Desktop helper methods static InputCombo Keyboard(uint32_t key) { return InputCombo(InputDeviceType::Keyboard, key); } static InputCombo Mouse(uint32_t key) { return InputCombo(InputDeviceType::Mouse, key); } static InputCombo Gamepad(uint32_t key) { return InputCombo(InputDeviceType::Gamepad, key); } @@ -185,12 +192,84 @@ struct InputCombo * If so, we could serialize as a list of integers (key codes) for better readability. */ + /** + * @brief Static helper to get a formatted string for a VR combo + */ + static std::string GetVRString(const std::vector& combo) + { + std::string result; + for (size_t i = 0; i < combo.size(); ++i) { + if (i > 0) + result += " + "; + const auto& input = combo[i]; + + if (input.GetDevice() == InputDeviceType::Keyboard) { + result += std::format("Key:{:X}", input.GetKey()); + } else { + // VR Button mapping + // Based on BSOpenVRControllerDevice::Keys enum values + // 2 = Grip, 7 = Trigger, 32 = Touchpad, 33 = Stick, 1 = BY, 31 = XA + // These are standard OpenVR / SkyrimVR constants + switch (input.GetKey()) { + case 2: + result += "Grip"; + break; + case 7: + result += "Trigger"; + break; + case 32: + result += "Touchpad"; + break; + case 33: + result += "Stick"; + break; + case 1: + result += "B/Y"; + break; + case 31: + result += "A/X"; + break; + case 9: + result += "Menu"; + break; + case 34: + result += "Shoulder"; + break; + default: + result += std::format("Btn:{:d}", input.GetKey()); + break; + } + + // Append device info if mixed or specific + switch (input.GetDevice()) { + case InputDeviceType::Primary: + result += "(Pri)"; + break; + case InputDeviceType::Secondary: + result += "(Sec)"; + break; + case InputDeviceType::Both: + result += "(Both)"; + break; + default: + break; + } + } + } + + if (result.empty()) { + return "None"; + } + + return result; + } + /** * @brief Wrapper for std::vector to provide custom JSON serialization. * * Serialization rules for backward compatibility: * - Single keyboard key (no modifiers): saves as plain uint32_t key code - * - Single controller input: saves as plain uint32_t packed value + * - Single VR/controller input: saves as plain uint32_t packed value * - Multiple keys (combo): saves as array of uint32_t values * - Empty: saves as 0 (unbound) * @@ -213,7 +292,7 @@ struct InputCombo // Single keyboard key - save as plain key code j = combos[0].GetKey(); } else { - // Single controller input - save as packed value + // Single VR/controller input - save as packed value j = combos[0].deviceAndKey; } return; @@ -241,7 +320,7 @@ struct InputCombo } j = keyCodes; } else { - // For mixed inputs, use the packed format + // For VR or mixed inputs, use the packed format std::vector packedValues; packedValues.reserve(combos.size()); for (const auto& c : combos) { diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 6f636b9735..bd20907d3d 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -25,9 +25,11 @@ #include #include "../Feature.h" +#include "../Features/VR.h" #include "../Globals.h" #include "../Menu.h" #include "FileSystem.h" +#include "VRUtils.h" #define STB_IMAGE_IMPLEMENTATION #include diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 7b2cb0a47a..884993c19d 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1209,7 +1209,7 @@ namespace Util * @brief Converts a key combo (vector of InputCombo) to a human-readable string * * For keyboard-only combos, produces strings like "Ctrl + Shift + A". - * For non-keyboard inputs, formats using the packed device+key representation. + * For VR inputs, delegates to InputCombo::GetVRString for proper formatting. * * @param combo Vector of InputCombo representing the key combination * @return Human-readable string representation of the combo, or "None" if empty @@ -1651,9 +1651,9 @@ namespace Util } /** - * @brief Unified input recording widget + * @brief Unified input recording widget for both VR and Desktop * - * Handles recording of multi-key sequences for keyboard and mouse. + * Handles recording of multi-key sequences for keyboard, mouse, and VR controllers. * Supports modifiers, combo sequences, and device-specific rendering. * * @param label The label for the input setting diff --git a/src/Utils/VRUtils.cpp b/src/Utils/VRUtils.cpp new file mode 100644 index 0000000000..23ac293156 --- /dev/null +++ b/src/Utils/VRUtils.cpp @@ -0,0 +1,241 @@ +#include "VRUtils.h" +#include "Features/VR.h" // For ButtonCombo and ControllerDevice definitions +#include "RE/B/BSOpenVR.h" +#include "UI.h" +#include + +namespace Util +{ + void DrawButtonCombo(const std::vector& combo, bool showControllerLabels) + { + bool anyDrawn = false; + for (size_t i = 0; i < combo.size(); ++i) { + if (combo[i].GetKey() == 0) + continue; + if (i > 0) { + ImGui::SameLine(); + ImGui::Text("+"); + ImGui::SameLine(); + } + ImVec4 color; + switch (combo[i].GetDevice()) { + case InputDeviceType::Primary: + color = Util::GetControllerPrimaryColor(); + break; + case InputDeviceType::Secondary: + color = Util::GetControllerSecondaryColor(); + break; + case InputDeviceType::Both: + color = Util::GetControllerBothColor(); + break; + default: + color = Util::GetControllerDefaultColor(); + break; + } + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::Text("%s", RE::GetOpenVRButtonName(combo[i].GetKey())); + ImGui::PopStyleColor(); + anyDrawn = true; + if (showControllerLabels) { + ImGui::SameLine(); + ImVec4 labelColor = Util::GetControllerDefaultColor(); + const char* label = ""; + switch (combo[i].GetDevice()) { + case InputDeviceType::Primary: + label = "(Primary Controller)"; + labelColor = Util::GetControllerPrimaryColor(); + break; + case InputDeviceType::Secondary: + label = "(Secondary Controller)"; + labelColor = Util::GetControllerSecondaryColor(); + break; + case InputDeviceType::Both: + label = "(Both Controllers)"; + labelColor = Util::GetControllerBothColor(); + break; + default: + break; + } + ImGui::TextColored(labelColor, "%s", label); + if (i < combo.size() - 1) + ImGui::SameLine(); + } + } + if (anyDrawn) { + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawColoredMultiLineTooltip({ { "Color coding:", Util::GetControllerDefaultColor() }, + { "Yellow = Primary controller", Util::GetControllerPrimaryColor() }, + { "Blue = Secondary controller", Util::GetControllerSecondaryColor() }, + { "Green = Both controllers (Yellow + Blue)", Util::GetControllerBothColor() } }); + } + } + } + + vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ) + { + // Initialize as identity matrix to ensure valid transform on early returns + vr::HmdMatrix34_t transform = {}; + transform.m[0][0] = 1.0f; + transform.m[1][1] = 1.0f; + transform.m[2][2] = 1.0f; + // All other elements remain 0.0f from the {} initialization + + // Use the same OpenVR access pattern as the VR class + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr) + return transform; + + auto* system = openvr->vrSystem; + if (!system) + return transform; + + vr::TrackedDevicePose_t hmdPose; + if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, &hmdPose, 1)) + return transform; + if (!hmdPose.bPoseIsValid) + return transform; + + transform = hmdPose.mDeviceToAbsoluteTracking; + + // Apply HMD overlay offsets (in HMD local space) + transform.m[0][3] += transform.m[0][0] * offsetX + transform.m[0][1] * offsetY + transform.m[0][2] * offsetZ; + transform.m[1][3] += transform.m[1][0] * offsetX + transform.m[1][1] * offsetY + transform.m[1][2] * offsetZ; + transform.m[2][3] += transform.m[2][0] * offsetX + transform.m[2][1] * offsetY + transform.m[2][2] * offsetZ; + + return transform; + } + + //============================================================================= + // NEW ACTIVE FUNCTIONS FROM VR.CPP + //============================================================================= + + // NOTE: OpenComposite Compatibility + // The functions below provide compatibility with OpenComposite, which has issues + // with GetDeviceToAbsoluteTrackingPose when requesting poses. We completely avoid + // using GetDeviceToAbsoluteTrackingPose and instead use VRCompositor interfaces + // obtained through BSOpenVR to avoid static linking issues on non-VR systems. + + OpenVRContext::OpenVRContext() + { + openvr = RE::BSOpenVR::GetSingleton(); + if (openvr) { + system = openvr->vrSystem; + overlay = RE::BSOpenVR::GetIVROverlayFromContext(&openvr->vrContext); + } + } + + vr::TrackedDeviceIndex_t GetControllerIndexForDevice(InputDeviceType device, bool isLeftHanded) + { + OpenVRContext ctx; + if (!ctx.IsValid()) + return vr::k_unTrackedDeviceIndexInvalid; + + // Determine the OpenVR role based on handedness and our device enum + vr::ETrackedControllerRole targetRole; + + if (device == InputDeviceType::Primary) { + // Primary controller = dominant hand + targetRole = isLeftHanded ? vr::ETrackedControllerRole::TrackedControllerRole_LeftHand : vr::ETrackedControllerRole::TrackedControllerRole_RightHand; + } else { + // Secondary controller = non-dominant hand + targetRole = isLeftHanded ? vr::ETrackedControllerRole::TrackedControllerRole_RightHand : vr::ETrackedControllerRole::TrackedControllerRole_LeftHand; + } + + // Find controller with the target role + for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (ctx.system->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller) { + if (ctx.system->GetControllerRoleForTrackedDeviceIndex(i) == targetRole) { + return i; + } + } + } + return vr::k_unTrackedDeviceIndexInvalid; + } + + bool GetControllerWorldMatrix(vr::TrackedDeviceIndex_t index, float out[3][4]) + { + OpenVRContext ctx; + if (!ctx.IsValid()) + return false; + + vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]; + if (!GetDeviceToAbsoluteTrackingPoseCompatible(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount)) + return false; + + if (!poses[index].bPoseIsValid) + return false; + + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 4; ++j) + out[i][j] = poses[index].mDeviceToAbsoluteTracking.m[i][j]; + return true; + } + + bool GetDeviceToAbsoluteTrackingPoseCompatible(vr::ETrackingUniverseOrigin eOrigin, float fPredictedSecondsToPhotonsFromNow, vr::TrackedDevicePose_t* pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount) + { + (void)fPredictedSecondsToPhotonsFromNow; + (void)eOrigin; + OpenVRContext ctx; + if (!ctx.IsValid()) + return false; + + // For single device requests (common with HMD pose requests), + // use a full pose array to ensure OpenComposite compatibility + if (unTrackedDevicePoseArrayCount == 1) { + vr::TrackedDevicePose_t allPoses[vr::k_unMaxTrackedDeviceCount]; + + // Try to use compositor interface first for better OpenComposite compatibility + // Use BSOpenVR's method to avoid static linking issues + auto* compositor = RE::BSOpenVR::GetIVRCompositor(); + if (!compositor && ctx.openvr) { + // Fallback to compositor from the context + compositor = ctx.openvr->vrContext.vrCompositor; + } + + if (compositor) { + // For OpenComposite compatibility, try to use GetLastPoses which is more stable + auto error = compositor->GetLastPoses(allPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + if (error == vr::VRCompositorError_None) { + // Copy HMD pose (index 0) to output + pTrackedDevicePoseArray[0] = allPoses[0]; + return true; + } + // Fallback to WaitGetPoses if GetLastPoses fails + error = compositor->WaitGetPoses(allPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + if (error == vr::VRCompositorError_None) { + // Copy HMD pose (index 0) to output + pTrackedDevicePoseArray[0] = allPoses[0]; + return true; + } + } + + // If compositor methods failed, return false rather than using the problematic direct call + return false; + } + + // For full device array requests, try compositor first + // Use BSOpenVR's method to avoid static linking issues + auto* compositor = RE::BSOpenVR::GetIVRCompositor(); + if (!compositor && ctx.openvr) { + // Fallback to compositor from the context + compositor = ctx.openvr->vrContext.vrCompositor; + } + + if (compositor) { + // For OpenComposite compatibility, try to use GetLastPoses which is more stable + auto error = compositor->GetLastPoses(pTrackedDevicePoseArray, unTrackedDevicePoseArrayCount, nullptr, 0); + if (error == vr::VRCompositorError_None) { + return true; + } + // Fallback to WaitGetPoses if GetLastPoses fails + error = compositor->WaitGetPoses(pTrackedDevicePoseArray, unTrackedDevicePoseArrayCount, nullptr, 0); + if (error == vr::VRCompositorError_None) { + return true; + } + } + + // If compositor methods failed, return false rather than using the problematic direct call + return false; + } + +} \ No newline at end of file diff --git a/src/Utils/VRUtils.h b/src/Utils/VRUtils.h new file mode 100644 index 0000000000..0e52b283d6 --- /dev/null +++ b/src/Utils/VRUtils.h @@ -0,0 +1,248 @@ +#pragma once +#include "D3D.h" +#include "Utils/Input.h" +#include +#include +#include // For ImVec4 +#include +#include + +// Forward declarations - actual definitions are in Features/VR.h +using ControllerDevice = InputDeviceType; +using ButtonCombo = InputCombo; + +/** + * @brief VR utility functions and helpers for OpenVR integration + * + * This namespace provides a collection of utility functions for VR development, + * including overlay management, matrix transformations, controller utilities, + * and UI drawing functions for VR-specific elements. + */ +namespace Util +{ + // ----------------------------------------------------------------------------- + // Centralized UI Colors for Util functions + // ----------------------------------------------------------------------------- + namespace Colors + { + constexpr ImVec4 Primary = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow + constexpr ImVec4 Secondary = ImVec4(0.0f, 0.5f, 1.0f, 1.0f); // Blue + constexpr ImVec4 Both = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green + constexpr ImVec4 Default = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White + } + + inline ImVec4 GetControllerPrimaryColor() { return Colors::Primary; } + inline ImVec4 GetControllerSecondaryColor() { return Colors::Secondary; } + inline ImVec4 GetControllerBothColor() { return Colors::Both; } + inline ImVec4 GetControllerDefaultColor() { return Colors::Default; } + + /** + * @brief Draws a button combination in the ImGui interface with color coding + * @param combo Vector of ButtonCombo structures representing the key combination + * @param showControllerLabels Whether to show controller device labels (Primary/Secondary/Both) + * + * This function renders button combinations with color-coded text: + * - Green: Primary controller + * - Blue: Secondary controller + * - Purple: Both controllers + * + * @example + * ```cpp + * std::vector combo = { ButtonCombo::Primary(kTrigger), ButtonCombo::Secondary(kGrip) }; + * Util::DrawButtonCombo(combo, true); + * ``` + */ + void DrawButtonCombo(const std::vector& combo, bool showControllerLabels); + + /** + * @brief Computes a transformation matrix for positioning an overlay relative to the HMD + * @param offsetX Horizontal offset from HMD in meters (positive = right) + * @param offsetY Vertical offset from HMD in meters (positive = up) + * @param offsetZ Depth offset from HMD in meters (positive = away from user) + * @return HMD transformation matrix with applied offsets + * + * This function gets the current HMD pose and applies the specified offsets + * in HMD local space to create a transformation matrix suitable for overlay positioning. + */ + vr::HmdMatrix34_t ComputeOverlayTransformFromHMD(float offsetX, float offsetY, float offsetZ); + + /** + * @brief Common OpenVR system access pattern with validation + * + * This struct provides a standardized way to access OpenVR interfaces + * with proper validation and error handling. It encapsulates the common + * pattern of getting BSOpenVR singleton and extracting the system and overlay interfaces. + */ + struct OpenVRContext + { + RE::BSOpenVR* openvr = nullptr; ///< BSOpenVR singleton instance + vr::IVRSystem* system = nullptr; ///< OpenVR system interface + vr::IVROverlay* overlay = nullptr; ///< OpenVR overlay interface + + /** + * @brief Constructor that initializes all OpenVR interfaces + * + * Automatically retrieves the BSOpenVR singleton and extracts + * the system and overlay interfaces for immediate use. + */ + OpenVRContext(); + + /** + * @brief Check if basic VR system is available + * @return true if both openvr and system interfaces are valid + */ + bool IsValid() const { return openvr && system; } + + /** + * @brief Check if overlay functionality is available + * @return true if all interfaces (including overlay) are valid + */ + bool HasOverlay() const { return IsValid() && overlay; } + }; + + /** + * @brief Get controller index for our ControllerDevice enum + * @param device The controller device type (Primary/Secondary) + * @param isLeftHanded Whether the user is left-handed (affects primary/secondary mapping) + * @return Tracked device index or vr::k_unTrackedDeviceIndexInvalid if not found + * + * This function maps our ControllerDevice enum to actual OpenVR tracked device indices, + * taking into account user handedness for primary/secondary controller assignment. + */ + vr::TrackedDeviceIndex_t GetControllerIndexForDevice(ControllerDevice device, bool isLeftHanded); + + /** + * @brief Get controller world matrix from OpenVR pose + * @param index The tracked device index of the controller + * @param out Output array[3][4] for the transformation matrix + * @return true if the pose was valid and matrix was retrieved successfully + * + * This function retrieves the current world-space transformation matrix + * for a VR controller in a format compatible with OpenVR matrix operations. + */ + bool GetControllerWorldMatrix(vr::TrackedDeviceIndex_t index, float out[3][4]); + + /** + * @brief OpenComposite-compatible function to get device poses + * @param eOrigin The tracking universe origin + * @param fPredictedSecondsToPhotonsFromNow Prediction time for poses + * @param pTrackedDevicePoseArray Output array for tracked device poses + * @param unTrackedDevicePoseArrayCount Number of poses to retrieve + * @return true if poses were retrieved successfully + * + * This function provides a compatibility layer for getting device poses that works + * with both standard OpenVR and OpenComposite. It uses the compositor interface + * when available for better OpenComposite compatibility. + */ + bool GetDeviceToAbsoluteTrackingPoseCompatible(vr::ETrackingUniverseOrigin eOrigin, float fPredictedSecondsToPhotonsFromNow, vr::TrackedDevicePose_t* pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount); + + //============================================================================= + // MATRIX CONVERSION UTILITIES + //============================================================================= + + /** + * @brief Converts an OpenVR HmdMatrix34_t to a DirectX SimpleMath Matrix + * @param m The OpenVR 3x4 transformation matrix to convert + * @return DirectX SimpleMath 4x4 Matrix with bottom row set to [0,0,0,1] + * + * This function converts between OpenVR's 3x4 transformation matrix format + * and DirectX SimpleMath's 4x4 matrix format, adding the implicit bottom row. + */ + inline Matrix HmdMatrix34ToMatrix(const vr::HmdMatrix34_t& m) + { + // OpenVR matrices are row-major but designed for column-vector math (M * v). + // DirectX SimpleMath uses row-vector math (v * M). + // We need to transpose the rotation and move translation to the bottom row. + return Matrix( + m.m[0][0], m.m[1][0], m.m[2][0], 0.0f, + m.m[0][1], m.m[1][1], m.m[2][1], 0.0f, + m.m[0][2], m.m[1][2], m.m[2][2], 0.0f, + m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); + } + + /** + * @brief Converts a DirectX SimpleMath Matrix to an OpenVR HmdMatrix34_t + * @param mat The DirectX SimpleMath 4x4 matrix to convert + * @return OpenVR 3x4 transformation matrix (bottom row is discarded) + * + * This function converts from DirectX SimpleMath's 4x4 matrix format + * to OpenVR's 3x4 transformation matrix format, discarding the bottom row. + */ + inline vr::HmdMatrix34_t MatrixToHmdMatrix34(const Matrix& mat) + { + vr::HmdMatrix34_t m{}; + // Transpose rotation back (row-vector → column-vector) and extract translation from row 4 + m.m[0][0] = mat._11; + m.m[0][1] = mat._21; + m.m[0][2] = mat._31; + m.m[0][3] = mat._41; + m.m[1][0] = mat._12; + m.m[1][1] = mat._22; + m.m[1][2] = mat._32; + m.m[1][3] = mat._42; + m.m[2][0] = mat._13; + m.m[2][1] = mat._23; + m.m[2][2] = mat._33; + m.m[2][3] = mat._43; + return m; + } + + /** + * @brief Converts a raw 3x4 float array to an OpenVR HmdMatrix34_t + * @param m Raw 3x4 float array in [row][column] format + * @return OpenVR HmdMatrix34_t structure + * + * This function provides a convenient way to convert raw transformation + * matrices from other APIs into OpenVR's matrix format. + */ + inline vr::HmdMatrix34_t Float3x4ToHmdMatrix34(const float m[3][4]) + { + vr::HmdMatrix34_t mat; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 4; ++j) + mat.m[i][j] = m[i][j]; + return mat; + } + + /** + * @brief Gets the Inter-Pupillary Distance (IPD) from the HMD + * @return IPD in meters, or 0.064 (average human IPD) as fallback + * + * Tries multiple methods to determine IPD: + * 1. Query Prop_UserIpdMeters_Float property directly + * 2. Calculate from eye-to-head transforms + * 3. Fallback to average human IPD (64mm) + */ + inline float GetIPDFromHMD() + { + RE::BSOpenVR* openvr = RE::BSOpenVR::GetSingleton(); + if (!openvr || !openvr->vrSystem) + return 0.064f; // Default fallback IPD in meters + + // Method 1: Query IPD property directly + vr::ETrackedPropertyError error = vr::TrackedProp_UnknownProperty; + float ipd = openvr->vrSystem->GetFloatTrackedDeviceProperty( + vr::k_unTrackedDeviceIndex_Hmd, + vr::Prop_UserIpdMeters_Float, + &error); + + if (error == vr::TrackedProp_Success && ipd > 0.0f && ipd < 0.1f) { + return ipd; + } + + // Method 2: Calculate from eye-to-head transforms + vr::HmdMatrix34_t leftEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Left); + vr::HmdMatrix34_t rightEye = openvr->vrSystem->GetEyeToHeadTransform(vr::Eye_Right); + + // Eye separation is in the X translation component (m[0][3]) + float eyeSeparation = std::abs(leftEye.m[0][3] - rightEye.m[0][3]); + + if (eyeSeparation > 0.0f && eyeSeparation < 0.1f) { + return eyeSeparation; + } + + // Fallback to average human IPD + return 0.064f; + } + +} \ No newline at end of file From b6e1065354c76726cd6c5f317bd1881734ebee7a Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 16:22:34 -0700 Subject: [PATCH 45/55] =?UTF-8?q?=EF=BB=BFfix:=20resolve=20v1.7.0=20sync?= =?UTF-8?q?=20compile=20cascades?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixups making the merge+revert compile clean (ALL preset, 0 warnings): - Feature.h: drop duplicate SupportsVR (merge restored it; revert re-added the canonical one -> redefinition). - InverseSquareLighting.h: add CSEditor/LightEditor.h include (rename broke the transitive path WeatherEditor/ -> CSEditor/). - FidelityFX.cpp / Hooks.cpp: remove duplicated Upscale() body and CreateRenderTarget_Snow/_SnowSwap structs (revert re-added older copies beside our kept versions). - HomePageRenderer.cpp: drop stray ')' (our plain introText kept, upstream T() close auto-merged). - ScreenshotFeature.h + Hooks.cpp: drop upstream ProcessCaptureRequest decl/call (our screenshot path uses Capture()+worker thread; HDR-PNG rework deferred). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Feature.h | 3 -- src/Features/InverseSquareLighting.h | 1 + src/Features/ScreenshotFeature.h | 2 - src/Features/Upscaling/FidelityFX.cpp | 75 --------------------------- src/Hooks.cpp | 24 --------- src/Menu/HomePageRenderer.cpp | 2 +- 6 files changed, 2 insertions(+), 105 deletions(-) diff --git a/src/Feature.h b/src/Feature.h index a94dcaed9e..633a26fa1b 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -68,9 +68,6 @@ struct Feature virtual std::string GetName() = 0; virtual std::string GetShortName() = 0; virtual std::string GetDisplayName() { return GetName(); } - // Restored after upstream #2475 removed it: VR features (and the SupportsVR gate in - // Feature.cpp / RemoteControl) rely on this; this fork keeps VR. - virtual bool SupportsVR() { return false; } std::string GetDisplayCategory() const; virtual std::string GetFeatureModLink() { return ""; } virtual std::string_view GetShaderDefineName() { return ""; } diff --git a/src/Features/InverseSquareLighting.h b/src/Features/InverseSquareLighting.h index 37a56dfb70..465fc36b28 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -1,4 +1,5 @@ #pragma once +#include "CSEditor/LightEditor.h" #include "LightLimitFix.h" struct InverseSquareLighting : Feature diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index c5a87cb8f7..84e8c11595 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -28,8 +28,6 @@ struct ScreenshotFeature : public Feature virtual void PostPostLoad() override; void Capture(); - // Runs after HDR Present processing so the back buffer matches what's on screen. - void ProcessCaptureRequest(); bool applyCropToScreenshot = true; // Settings diff --git a/src/Features/Upscaling/FidelityFX.cpp b/src/Features/Upscaling/FidelityFX.cpp index 10adcbab2f..e22ae987b9 100644 --- a/src/Features/Upscaling/FidelityFX.cpp +++ b/src/Features/Upscaling/FidelityFX.cpp @@ -458,79 +458,4 @@ void FidelityFX::Upscale(ID3D11Resource* a_upscalingTexture, ID3D11Resource* a_r (uint)renderSize.x, renderSize.x); } - - FfxFsr3DispatchUpscaleDescription dispatchParameters{}; - dispatchParameters.commandList = ffxGetCommandListDX11(context); - dispatchParameters.color = ffxGetResource(r_color, L"FSR3_Input_OutputColor"); - dispatchParameters.depth = ffxGetResource(r_depth, L"FSR3_InputDepth"); - dispatchParameters.motionVectors = ffxGetResource(r_mvec, L"FSR3_InputMotionVectors"); - dispatchParameters.exposure = ffxGetResource(nullptr, L"FSR3_InputExposure"); - dispatchParameters.upscaleOutput = ffxGetResource(r_output, L"FSR3_OutputColor"); - dispatchParameters.reactive = ffxGetResource(r_reactive, L"FSR3_InputReactiveMap"); - dispatchParameters.transparencyAndComposition = ffxGetResource(r_trans, L"FSR3_TransparencyAndCompositionMap"); - - dispatchParameters.motionVectorScale.x = mv_scale_x; - dispatchParameters.motionVectorScale.y = renderSize.y; - dispatchParameters.renderSize.width = r_width; - dispatchParameters.renderSize.height = (uint)renderSize.y; - - dispatchParameters.jitterOffset.x = -jitter.x; - dispatchParameters.jitterOffset.y = -jitter.y; - - dispatchParameters.frameTimeDelta = *globals::game::deltaTime * 1000.f; - dispatchParameters.cameraFar = *globals::game::cameraFar; - dispatchParameters.cameraNear = *globals::game::cameraNear; - dispatchParameters.enableSharpening = true; - dispatchParameters.sharpness = a_sharpness; - dispatchParameters.cameraFovAngleVertical = Util::GetVerticalFOVRad(); - dispatchParameters.viewSpaceToMetersFactor = 0.01428222656f; - dispatchParameters.reset = false; - dispatchParameters.preExposure = 1.0f; - dispatchParameters.flags = 0; - - __try { - if (ffxFsr3ContextDispatchUpscale(&fsrContext[contextIndex], &dispatchParameters) != FFX_OK) - logger::critical("[FidelityFX] Failed to dispatch upscaling for eye {}!", contextIndex); - } __except (EXCEPTION_EXECUTE_HANDLER) { - if (!fsrDispatchCrashLogged) { - logger::critical("[FidelityFX] FSR3 dispatch crashed for eye {} - this may be caused by RenderDoc capture interfering with FSR operations. Try disabling RenderDoc capture.", contextIndex); - fsrDispatchCrashLogged = true; - } - } - - if (state->frameAnnotations) - state->EndPerfEvent(); - }; - - if (globals::game::isVR) { - // Prepare per-eye inputs and clear mask - upscaling.PreparePerEyeInputs(a_upscalingTexture); - - uint32_t numViews = 2; - uint32_t eyeWidth = (uint32_t)(renderSize.x / 2); - for (uint32_t i = 0; i < numViews; ++i) { - DispatchFSR(i, - upscaling.vrIntermediateColorIn[i]->resource.get(), - upscaling.vrIntermediateLinearDepth[i]->resource.get(), - upscaling.vrIntermediateMotionVectors[i]->resource.get(), - upscaling.vrIntermediateReactiveMask[i]->resource.get(), - upscaling.vrIntermediateTransparencyMask[i]->resource.get(), - upscaling.vrIntermediateColorOut[i]->resource.get(), - eyeWidth, - renderSize.x / 2.0f); - } - - // Merge outputs back to kMAIN - upscaling.FinalizePerEyeOutputs(a_upscalingTexture); - } else { - DispatchFSR(0, - a_upscalingTexture, - depthTexture.texture, - a_motionVectors, - a_reactiveMask, - a_transparencyCompositionMask, - a_upscalingTexture, // Output to same texture - (uint)renderSize.x, - renderSize.x); - } } \ No newline at end of file diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 0c425e4eab..9e9e66524e 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -268,8 +268,6 @@ struct IDXGISwapChain_Present return func(swapChain, syncInterval, presentFlags); }); - globals::features::screenshotFeature.ProcessCaptureRequest(); - TracyD3D11Collect(globals::state->tracyCtx); return retval; @@ -571,28 +569,6 @@ namespace Hooks // kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader // writes accumulated wetness/sparkle values that exceed the 8-bit range and // quantize into visible banding on tessellated snow. Promote to fp16 for headroom. - struct CreateRenderTarget_Snow - { - static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) - { - auto properties = *a_properties; - properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); - func(This, a_target, &properties); - } - static inline REL::Relocation func; - }; - - struct CreateRenderTarget_SnowSwap - { - static void thunk(RE::BSGraphics::Renderer* This, RE::RENDER_TARGETS::RENDER_TARGET a_target, RE::BSGraphics::RenderTargetProperties* a_properties) - { - auto properties = *a_properties; - properties.format.set(RE::BSGraphics::Format::kR16G16B16A16_FLOAT); - func(This, a_target, &properties); - } - static inline REL::Relocation func; - }; - // kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader // writes accumulated wetness/sparkle values that exceed the 8-bit range and // quantize into visible banding on tessellated snow. Promote to fp16 for headroom. diff --git a/src/Menu/HomePageRenderer.cpp b/src/Menu/HomePageRenderer.cpp index 3ee8a0d0a8..09a41cf4d1 100644 --- a/src/Menu/HomePageRenderer.cpp +++ b/src/Menu/HomePageRenderer.cpp @@ -86,7 +86,7 @@ void HomePageRenderer::RenderWelcomeSection() const char* introText = "Open Shaders is a fork of Community Shaders providing advanced graphics enhancements for Skyrim.\n" "This comprehensive collection of features brings modern rendering techniques\n" - "to enhance your visual experience."); + "to enhance your visual experience."; ImVec2 introSize = ImGui::CalcTextSize(introText); ImGui::SetCursorPosX((windowSize.x - introSize.x) * 0.5f); ImGui::TextWrapped("%s", introText); From 74300488e18a5ea379d93f946105fe5546144734 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 16:56:25 -0700 Subject: [PATCH 46/55] =?UTF-8?q?=EF=BB=BFfix:=20VR=20regressions=20found?= =?UTF-8?q?=20by=20sync=20audit=20+=20bigobj?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallel upstream/fork audits of the v1.7.0 merge surfaced two VR regressions invisible to the C++ build (HLSL not compiled there; the offsets are runtime): - ISTemporalAA.hlsl: drop orphaned #endif (revert artifact) that unbalanced the preprocessor and would fail CI shader validation / TAA at runtime. - Hooks.cpp: restore the VR (3rd) REL::Relocate offset on three render-target thunks the silent #2475 strip removed (precip-mask depth 0x1917, cubemap reflections 0xCD2, depth reflections 0xD13) -- merge took the 2-arg form on non-conflicting lines and the revert didn't touch Hooks.cpp. - CMakeLists: add /bigobj (LLF ShadowCasterManager.cpp exceeds the COFF section limit on a fresh compile after the merge grew its includes). Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 3 +++ package/Shaders/ISTemporalAA.hlsl | 1 - src/Hooks.cpp | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7211305163..03c0f6d499 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,9 @@ if(MSVC) "$<$:/LTCG>" "$<$:/INCREMENTAL:NO>" ) + # Large translation units (e.g. LightLimitFix/ShadowCasterManager.cpp pulling the + # full feature + editor headers) exceed the default COFF section limit; /bigobj. + target_compile_options(${PROJECT_NAME} PRIVATE "/bigobj") endif() # https://gitlab.kitware.com/cmake/cmake/-/issues/24922#note_1371990 diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index cf90a30f03..4b71469bf0 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -537,7 +537,6 @@ PS_OUTPUT main(PS_INPUT input) feedbackOut.x = feedbackLumaOut; // Vanilla writes opaque alpha unconditionally on both SE and VR (decompile o0.w = 1). colorOut.w = 1; -# endif feedbackOut.w = 1; # ifdef HDR_OUTPUT diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 9e9e66524e..8030bb1316 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -1034,9 +1034,9 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF4F, 0xF51)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF65, 0xF67)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B, 0x1917)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25, 0xCD2)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59, 0xD13)); globals::features::upscaling.perfMode.InstallCreateRTThunks(); From aae39809601759acd2b29c8012978fd09ff9ac45 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 18:31:06 -0700 Subject: [PATCH 47/55] =?UTF-8?q?=EF=BB=BFfix(vr):=20RE=20Water=20RT=20VR?= =?UTF-8?q?=20offsets=20+=20dedup=20depth=20propagate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VR runtime smoke (SteamVR null-driver headless rig) caught a real CTD the DLL/SE-smoke gates couldn't: an EXCEPTION_ACCESS_VIOLATION in BSShaderRenderTargets::Create (id 100458) +0xF4B. - Hooks.cpp: upstream's new CreateRenderTarget_Water1/Water2 hooks (#2484, water RT fp16 promotion) shipped 2-arg REL::Relocate (no VR offset). On VR they wrote a CALL at the SE offset inside Create -> corrupted the function -> AV on the render thread. RE'd the VR call sites in SkyrimVR.exe Create (targets 0x59/0x5a): Water1 VR +0x12C2, Water2 VR +0x12D8 (cross-checked monotonic vs UnderwaterMask 0xE06 < Water 0x12C2/0x12D8 < PrecipitationMask 0x1917). Now 3-arg, works on VR. - Upscaling.cpp: drop the duplicate "Depth VR Propagate" block the #2475 revert re-added beside our existing one (dev had exactly one, gated isVR && depthUpscaleActive). The sacred propagate stays; only the redundant copy removed. A parallel fork-feature-loss audit (dev 6728765f3 vs HEAD) found NO genuine fork losses: all removed capabilities were either relocated/preserved or upstream PR removals (e.g. SkySync Sun Position Offsets removed by upstream #2408 -- evolution, not loss). All fork features (VR, foveated, LLF superset, branding, UAF hooks, #115, sacred propagate) confirmed intact. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Features/Upscaling.cpp | 6 ------ src/Hooks.cpp | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 8397ea114c..844ff4ef39 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -2523,12 +2523,6 @@ void Upscaling::UpscaleDepth() copyIfNonAliased(depthCopy.texture, depth.texture); } - // Now propagate the upscaled depth to kMAIN_COPY so downstream VR passes see it. - if (globals::game::isVR) { - TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Depth VR Propagate"); - copyIfNonAliased(depthCopy.texture, depth.texture); - } - ID3D11ShaderResourceView* nullPSResources[3] = { nullptr, nullptr, nullptr }; context->PSSetShaderResources(0, ARRAYSIZE(nullPSResources), nullPSResources); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 8030bb1316..51566c5b29 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -1031,8 +1031,8 @@ namespace Hooks stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x503, 0x502, 0x661)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xB19, 0xB19, 0xE06)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF4F, 0xF51)); - stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF65, 0xF67)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF4F, 0xF51, 0x12C2)); + stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xF65, 0xF67, 0x12D8)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0x1245, 0x123B, 0x1917)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA25, 0xA25, 0xCD2)); From d7e70c577b6afef9ee8a9ee1ce11efb0afddd709 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 18:48:58 -0700 Subject: [PATCH 48/55] =?UTF-8?q?=EF=BB=BFfeat(skysync):=20restore=20altit?= =?UTF-8?q?ude=20correction=20over=20upstream=20#2408?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hybridize SkySync per the upstream-merge strategy: keep upstream #2408's rework (moon-intensity sliders, DimSunlightUnderHorizon, climate-aware dim, Sky_UpdateColors, direction-lerp fader) AND restore the original author's altitude (horizon-dip) correction that #2408 deleted while still advertising "fixes the sun appearing higher when you gain altitude". GetApparentDirection (Rodrigues rotation by -atan(altitude/RenderDistance)) + GetPlayerAltitude are re-added as additive helpers, applied with one line each inside #2408's ProcessSun/ProcessMoon (altitude computed internally so #2408's function signatures are untouched -> future upstream SkySync merges stay clean). Left #2408's climate-aware dim + fixed-duration fader as-is (its currentDim is already weather-timed) to avoid rewriting the fader and creating bad merge diffs. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Features/SkySync.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/Features/SkySync.h | 8 ++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Features/SkySync.cpp b/src/Features/SkySync.cpp index 579611fde8..0c4dbcf188 100644 --- a/src/Features/SkySync.cpp +++ b/src/Features/SkySync.cpp @@ -303,6 +303,40 @@ void SkySync::SetSkyRotation(const RE::Sky* sky, RE::TESObjectCELL* cell) sky->root->Update(updateData); } +// --- Open Shaders fork: horizon-dip altitude correction, restored from the pre-#2408 SkySync. +// Rotates a celestial direction down by the apparent horizon dip for the player's elevation, so +// the sun/moons (and the shadow direction derived from them) don't sit too high when the player +// gains altitude. Upstream #2408 deleted this but still advertises the fix. Kept as additive +// helpers applied inside ProcessSun/ProcessMoon so upstream #2408 merges stay clean. +float SkySync::GetPlayerAltitude() +{ + const auto player = RE::PlayerCharacter::GetSingleton(); + if (!player) + return 0.0f; + const auto worldSpace = player->GetWorldspace(); + return worldSpace ? player->GetPositionZ() - worldSpace->GetDefaultWaterHeight() : 0.0f; +} + +RE::NiPoint3 SkySync::GetApparentDirection(const RE::NiPoint3& dir, const float altitude) +{ + const float dipAngle = -std::atan(altitude / RenderDistance); + float sinPhi, cosPhi; + DirectX::XMScalarSinCosEst(&sinPhi, &cosPhi, dipAngle); + + const auto rotationAxis = dir.UnitCross({ 0.0f, 0.0f, 1.0f }); + const float axisDotDir = rotationAxis.Dot(dir); + const auto axisCrossDir = rotationAxis.Cross(dir); + const float oneMinusCosPhi = 1.0f - cosPhi; + + const float x = dir.x * cosPhi + axisCrossDir.x * sinPhi + rotationAxis.x * (axisDotDir * oneMinusCosPhi); + const float y = dir.y * cosPhi + axisCrossDir.y * sinPhi + rotationAxis.y * (axisDotDir * oneMinusCosPhi); + const float z = dir.z * cosPhi + axisCrossDir.z * sinPhi + rotationAxis.z * (axisDotDir * oneMinusCosPhi); + + RE::NiPoint3 rotated = { x, y, z }; + rotated.Unitize(); + return rotated; +} + void SkySync::ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensities[]) { const auto sun = sky->sun; @@ -317,6 +351,8 @@ void SkySync::ProcessSun(const RE::Sky* sky, RE::NiPoint3 dirs[], float intensit } else CalculateSunDirectionAndDistance(sun, dir, dist); + dir = GetApparentDirection(dir, GetPlayerAltitude()); // fork: altitude correction + SetSunPosition(sun, dir, dist); dirs[static_cast(Caster::Sun)] = dir; @@ -339,6 +375,8 @@ void SkySync::ProcessMoon(const RE::Sky* sky, const Caster type, RE::NiPoint3 di if (moonAndStarsLoaded) dir = { dir.y, -dir.x, dir.z }; + dir = GetApparentDirection(dir, GetPlayerAltitude()); // fork: altitude correction + dirs[idx] = dir; const float4& baseColor = type == Caster::Masser ? Util::Moon::MasserBaseColor : Util::Moon::SecundaBaseColor; diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 2290fe1819..53bd73727a 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -150,4 +150,12 @@ struct SkySync : Feature static void CalculateAlternateSunDirectionAndDistance(RE::NiPoint3& outDir, float& outDist, float time, float sunrise, float sunset, float sunAngle); static void SetSunPosition(const RE::Sun* sun, const RE::NiPoint3& dir, float distance); + + // --- Open Shaders fork: altitude (horizon-dip) correction restored from the pre-#2408 + // SkySync. Upstream #2408 deleted this yet still lists "fixes the sun appearing higher + // when you gain altitude" in its feature summary. Re-implemented as an additive helper + // applied inside ProcessSun/ProcessMoon so upstream #2408 merges stay clean. + static constexpr float RenderDistance = 325000.0f; + static RE::NiPoint3 GetApparentDirection(const RE::NiPoint3& dir, float altitude); + static float GetPlayerAltitude(); }; From 36376be8b7ff718cec2055ad55393d028ae09ebb Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 19:08:33 -0700 Subject: [PATCH 49/55] feat(screenshot): adopt upstream HDR-PNG export onto VR Subrect path Hybridize upstream 1.7.0's HDR-PNG screenshot rework (#b74dedf962) with the fork's VR per-eye capture. Upstream and the fork independently grew the same worker-thread + Util::Subrect architecture, so this layers upstream's new capabilities onto our VR path rather than choosing one: - Adopt sk_hdr_png export, CF_HDROP clipboard copy, async ProcessCaptureRequest drain, OpaquePreviewBlendCallback, and the HdrPngBitDepth/SdrUsePng/ CopyToClipboard settings. - Re-add the fork's VR support as additive blocks (keeps upstream structure for clean future merges): SupportsVR()=true, and Left/Right/Full-Frame Subrect presets seeded in PostPostLoad (not LoadSettings -- Feature::Load only dispatches to LoadSettings when the JSON already has a settings block, so a fresh install would skip a seed placed there). - Hooks.cpp: call ProcessCaptureRequest after HDR Present so the captured back buffer matches what is on screen. Smoke-tested both platforms: SE (HDR display on) captured via the HDR composite back buffer to a valid PNG; VR (HDR off) captured via kFRAMEBUFFER with the Left Eye Subrect preset to a valid BMP. Save runs on the worker thread in both; no crash, no new log errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Features/ScreenshotFeature.cpp | 503 +++++++++++++++++++++++------ src/Features/ScreenshotFeature.h | 7 +- src/Hooks.cpp | 5 +- 3 files changed, 421 insertions(+), 94 deletions(-) diff --git a/src/Features/ScreenshotFeature.cpp b/src/Features/ScreenshotFeature.cpp index 4023101799..7296e2bfc4 100644 --- a/src/Features/ScreenshotFeature.cpp +++ b/src/Features/ScreenshotFeature.cpp @@ -1,22 +1,30 @@ // Screenshot Feature -// Non-blocking screenshot tool for flat (SE/AE) and VR. GPU copy runs on the +// Non-blocking screenshot tool. GPU copy runs on the // render thread; encoding and disk I/O run on a dedicated worker thread so // capture does not stall the frame. #include "Features/ScreenshotFeature.h" + +#include + #include "Features/HDRDisplay.h" +#include "Features/Upscaling.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Utils/FileSystem.h" -#include "Utils/Subrect.h" + +#define I18N_KEY_PREFIX "feature.screenshot." + #include -#include -#include -#include -#include +#pragma warning(push) +#pragma warning(disable: 4244) // double->float conversion in third-party header +#include +#pragma warning(pop) + #include -#include -#include +#include +#include namespace { @@ -142,9 +150,7 @@ namespace } } - // Tonemaps an FP16 linear scene-referred ScratchImage in-place: Reinhard - // c / (1 + c) for the luminance map, then gamma-2.2 for sRGB encoding. - // Approximates HDRDisplay's on-screen tonemap closely enough for SDR save. + // Tonemaps a linear RGB ScratchImage in-place: Reinhard c/(1+c), then gamma-2.2. void TonemapHdrToSrgb(DirectX::ScratchImage& image) { using namespace DirectX; @@ -157,7 +163,6 @@ namespace const XMVECTOR one = XMVectorSplatOne(); const XMVECTOR invGamma = XMVectorReplicate(1.0f / 2.2f); for (size_t i = 0; i < width; ++i) { - // Clamp negatives - some shaders emit tiny sub-zero values pow() would NaN on. XMVECTOR c = XMVectorMax(inPixels[i], XMVectorZero()); const XMVECTOR rgb = XMVectorDivide(c, XMVectorAdd(c, one)); const XMVECTOR gammaCorrected = XMVectorPow(rgb, invGamma); @@ -172,8 +177,6 @@ namespace const DirectX::Image* PrepareBmpImage(DirectX::ScratchImage& sourceImage, DirectX::ScratchImage& convertedImage) { - // FP16 sources carry HDR scene-referred values (peak >> 1.0) that BMP - // can't represent. Tonemap + gamma-encode before the 8-bit conversion. if (sourceImage.GetMetadata().format == DXGI_FORMAT_R16G16B16A16_FLOAT) { TonemapHdrToSrgb(sourceImage); } @@ -192,6 +195,98 @@ namespace return sourceImage.GetImage(0, 0, 0); } + // Game-root-relative paths (e.g. "Screenshots") must be absolute for CF_HDROP / Discord. + std::filesystem::path ResolveToAbsoluteGamePath(const std::filesystem::path& path) + { + if (path.is_absolute()) { + return path; + } + wchar_t buffer[MAX_PATH]{}; + const DWORD length = GetModuleFileNameW(nullptr, buffer, MAX_PATH); + if (length > 0 && length < MAX_PATH) { + return std::filesystem::path(buffer).parent_path() / path; + } + std::error_code ec; + return std::filesystem::absolute(path, ec); + } + + bool CopyFilePathToClipboardHDrop(const std::wstring& absolutePath) + { + if (absolutePath.empty()) { + return false; + } + + const size_t pathChars = absolutePath.size(); + const size_t bytes = sizeof(DROPFILES) + (pathChars + 2) * sizeof(wchar_t); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes); + if (!hMem) { + return false; + } + + auto* drop = static_cast(GlobalLock(hMem)); + if (!drop) { + GlobalFree(hMem); + return false; + } + + drop->pFiles = sizeof(DROPFILES); + drop->fWide = TRUE; + + auto* files = reinterpret_cast(reinterpret_cast(drop) + sizeof(DROPFILES)); + memcpy(files, absolutePath.c_str(), (pathChars + 1) * sizeof(wchar_t)); + + GlobalUnlock(hMem); + + for (int attempt = 0; attempt < 8; ++attempt) { + if (attempt > 0) { + Sleep(1 << (attempt - 1)); + } + if (!OpenClipboard(nullptr)) { + continue; + } + EmptyClipboard(); + const bool placed = SetClipboardData(CF_HDROP, hMem) != nullptr; + CloseClipboard(); + if (placed) { + return true; + } + } + + GlobalFree(hMem); + return false; + } + + void RunOnMainThread(std::function fn) + { + if (auto* taskInterface = SKSE::GetTaskInterface()) { + taskInterface->AddTask(std::move(fn)); + } else { + fn(); + } + } + + void CopySavedPathToClipboard(bool enabled, const std::filesystem::path& path) + { + if (!enabled || path.empty()) { + return; + } + + const auto absolutePath = ResolveToAbsoluteGamePath(path); + std::error_code ec; + if (!std::filesystem::exists(absolutePath, ec)) { + logger::warn("Screenshot not found for clipboard: {}", absolutePath.string()); + return; + } + if (std::filesystem::file_size(absolutePath, ec) == 0) { + logger::warn("Screenshot file is empty, skipping clipboard: {}", absolutePath.string()); + return; + } + + if (!CopyFilePathToClipboardHDrop(absolutePath.wstring())) { + logger::warn("Screenshot saved but clipboard copy failed."); + } + } + // Resolves the slot's underlying texture, falling back to QueryInterface on // SRV/RTV when slot.texture is null (kFRAMEBUFFER on flat aliases the swap- // chain backbuffer that way). `holder` keeps the QI refcount alive across @@ -223,14 +318,38 @@ namespace return resolveFromView(slot.RTV); } - // Picks the capture source by where ISHDR wrote the scene this frame: - // VR -> RE::RENDER_TARGETS::kVR_FRAMEBUFFER (SBS). - // HDR enabled -> HDR::HdrTexture (FP16 linear; PrepareBmpImage tonemaps). - // otherwise -> kFRAMEBUFFER (already tonemapped UNORM). - // - // HDR::OutputTexture is intentionally not used: on HDR10 swap chains it - // holds PQ-encoded values regardless of the enableHDR toggle, which save - // as washed-out BMPs without a color transform. + // Returns the texture that was presented to the display (post-ApplyHDR). + ID3D11Texture2D* ResolveDisplayedBackBuffer(winrt::com_ptr& holder) + { + auto& upscaling = globals::features::upscaling; + if (upscaling.d3d12SwapChainActive && + upscaling.dx12SwapChain.swapChainBufferWrapped && + upscaling.dx12SwapChain.swapChainBufferWrapped->resource11) { + holder.copy_from(upscaling.dx12SwapChain.swapChainBufferWrapped->resource11); + return holder.get(); + } + + if (!globals::d3d::swapChain) { + return nullptr; + } + + winrt::com_ptr backBuffer; + if (FAILED(globals::d3d::swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), backBuffer.put_void()))) { + return nullptr; + } + holder = std::move(backBuffer); + return holder.get(); + } + + bool IsFlatHdrScreenshotCapture() + { + return globals::features::hdrDisplay.loaded && + globals::features::hdrDisplay.settings.enableHDR; + } + + // Picks the capture source: + // HDR enabled -> swap-chain back buffer after ApplyHDR (PQ HDR10 / PQ float). + // otherwise -> kFRAMEBUFFER (tonemapped UNORM). CaptureSource SelectCaptureSource(winrt::com_ptr& holder) { CaptureSource src; @@ -239,19 +358,10 @@ namespace return src; } - if (globals::game::isVR) { - auto& slot = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kVR_FRAMEBUFFER]; - src.texture = ResolveSlotTexture(slot, holder); - src.srv = slot.SRV; - src.description = "VR SBS framebuffer"; - return src; - } - - auto& hdr = globals::features::hdrDisplay; - if (hdr.loaded && hdr.settings.enableHDR && hdr.hdrTexture && hdr.hdrTexture->resource) { - src.texture = hdr.hdrTexture->resource.get(); - src.srv = hdr.hdrTexture->srv.get(); - src.description = "HDR::HdrTexture (FP16 linear, will tonemap)"; + if (IsFlatHdrScreenshotCapture()) { + src.texture = ResolveDisplayedBackBuffer(holder); + src.needsPreviewCache = true; + src.description = "Swap chain back buffer (HDR display composite)"; return src; } @@ -274,16 +384,166 @@ namespace combo[0].GetKey() == VK_SNAPSHOT; } - std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath) + // Blend state used around the preview's ImGui::Image draw. Two regression + // risks if this is changed: + // 1. BlendEnable must stay FALSE - the source texture carries non-1 alpha + // where Skyrim composited UI plates; default SRC_ALPHA blend lets the + // host window background show through (visible on the desktop mirror). + // 2. WriteMask must exclude alpha (RGB only) to avoid compositing + // artifacts. RGB-only writes leave the plate's pre-cleared alpha=1 + // in place. + // Paired with ImDrawCallback_ResetRenderState queued by Subrect::DrawEditor + // immediately after the image draw. + void OpaquePreviewBlendCallback(const ImDrawList*, const ImDrawCmd*) + { + static winrt::com_ptr opaqueBlend; + if (!opaqueBlend) { + D3D11_BLEND_DESC desc{}; + desc.RenderTarget[0].BlendEnable = FALSE; + desc.RenderTarget[0].RenderTargetWriteMask = + D3D11_COLOR_WRITE_ENABLE_RED | + D3D11_COLOR_WRITE_ENABLE_GREEN | + D3D11_COLOR_WRITE_ENABLE_BLUE; + globals::d3d::device->CreateBlendState(&desc, opaqueBlend.put()); + } + if (opaqueBlend) { + globals::d3d::context->OMSetBlendState(opaqueBlend.get(), nullptr, 0xFFFFFFFF); + } + } + + std::filesystem::path BuildScreenshotPath(const std::string& screenshotPath, bool usePng) { SYSTEMTIME st; GetLocalTime(&st); char buf[80]; - snprintf(buf, sizeof(buf), "CS_%04d-%02d-%02d_%02d-%02d-%02d_%03d.bmp", + const char* extension = usePng ? ".png" : ".bmp"; + snprintf(buf, sizeof(buf), "CS_%04d-%02d-%02d_%02d-%02d-%02d_%03d%s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, - st.wMilliseconds); - return std::filesystem::path(screenshotPath) / buf; + st.wMilliseconds, + extension); + return ResolveToAbsoluteGamePath(std::filesystem::path(screenshotPath) / buf); + } + + struct HdrFormatInfo + { + DXGI_FORMAT dxgi; + sk_hdr_png::format png; + size_t bytesPerPixel; + }; + + constexpr HdrFormatInfo kHdrFormats[] = { + { DXGI_FORMAT_R10G10B10A2_UNORM, sk_hdr_png::format::r10g10b10a2_unorm, 4 }, + { DXGI_FORMAT_R16G16B16A16_FLOAT, sk_hdr_png::format::r16g16b16a16_pq, 8 }, + }; + + const HdrFormatInfo* LookupHdrFormat(DXGI_FORMAT format) + { + for (const auto& info : kHdrFormats) { + if (info.dxgi == format) { + return &info; + } + } + return nullptr; + } + + bool IsHdrCaptureFormat(DXGI_FORMAT format) + { + return LookupHdrFormat(format) != nullptr; + } + + // sk_hdr_png requires 16-byte aligned pixel memory. + bool CopyToAlignedPixelBuffer( + const DirectX::Image& image, + size_t bytesPerPixel, + void*& outAligned, + size_t& outByteSize) + { + if (bytesPerPixel == 0) { + return false; + } + + const size_t tightRowBytes = static_cast(image.width) * bytesPerPixel; + outByteSize = tightRowBytes * image.height; + + outAligned = _aligned_malloc(outByteSize, 16); + if (!outAligned) { + return false; + } + + auto* dest = static_cast(outAligned); + const auto* src = image.pixels; + for (size_t row = 0; row < image.height; ++row) { + memcpy(dest + row * tightRowBytes, src + row * image.rowPitch, tightRowBytes); + } + return true; + } + + bool SaveHdrPng( + const DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + int quantizationBits, + DXGI_FORMAT format) + { + const DirectX::Image* firstImage = image.GetImage(0, 0, 0); + const HdrFormatInfo* hdrInfo = firstImage ? LookupHdrFormat(format) : nullptr; + if (!firstImage || !hdrInfo || firstImage->format != format) { + return false; + } + + void* alignedPixels = nullptr; + size_t byteSize = 0; + if (!CopyToAlignedPixelBuffer(*firstImage, hdrInfo->bytesPerPixel, alignedPixels, byteSize)) { + return false; + } + + const bool saved = sk_hdr_png::write_image_to_disk( + outputPath.wstring().c_str(), + static_cast(firstImage->width), + static_cast(firstImage->height), + alignedPixels, + quantizationBits, + hdrInfo->png, + false); + + _aligned_free(alignedPixels); + return saved; + } + + bool SaveSdrScreenshot( + DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + bool saveAsPng) + { + StripAlphaForBmp(image); + DirectX::ScratchImage convertedImage; + const DirectX::Image* saveImage = PrepareBmpImage(image, convertedImage); + if (!saveImage) { + return false; + } + + const GUID& codec = saveAsPng ? + DirectX::GetWICCodec(DirectX::WIC_CODEC_PNG) : + DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP); + return SUCCEEDED(DirectX::SaveToWICFile( + *saveImage, + DirectX::WIC_FLAGS_NONE, + codec, + outputPath.c_str())); + } + + bool SaveScreenshotToDisk( + DirectX::ScratchImage& image, + const std::filesystem::path& outputPath, + DXGI_FORMAT format, + int hdrPngBitDepth, + bool saveAsHdrPng, + bool saveAsSdrPng) + { + if (saveAsHdrPng) { + return SaveHdrPng(image, outputPath, hdrPngBitDepth, format); + } + return SaveSdrScreenshot(image, outputPath, saveAsSdrPng); } } @@ -300,10 +560,9 @@ bool ScreenshotFeature::IsInMenu() const void ScreenshotFeature::PostPostLoad() { - // Seed VR-specific presets here rather than in LoadSettings: Feature::Load - // only dispatches to LoadSettings when the JSON already has a settings - // block, so a fresh install would skip a seed placed there. Left first so - // it's the initial selection (matches vanilla Skyrim VR's left-eye save). + // fork: seed VR per-eye Subrect presets here, not in LoadSettings -- Feature::Load only + // dispatches to LoadSettings when the JSON already has a settings block, so a fresh install + // would skip a seed placed there. Left Eye first so it's the default (matches vanilla VR saves). if (globals::game::isVR) { subrect.SeedDefaultPresets({ { .name = "Left Eye", .uv = { 0.0f, 0.0f, 0.5f, 1.0f } }, @@ -319,6 +578,12 @@ void ScreenshotFeature::LoadSettings(json& a_json) screenshotPath = a_json["ScreenshotPath"]; if (a_json.contains("ApplyCropToScreenshot")) applyCropToScreenshot = a_json["ApplyCropToScreenshot"]; + if (a_json.contains("HdrPngBitDepth")) + hdrPngBitDepth = std::clamp(a_json["HdrPngBitDepth"], 7u, 16u); + if (a_json.contains("SdrUsePng")) + sdrUsePng = a_json["SdrUsePng"]; + if (a_json.contains("CopyToClipboard")) + copyToClipboard = a_json["CopyToClipboard"]; subrect.LoadSettings(a_json); } @@ -327,28 +592,67 @@ void ScreenshotFeature::SaveSettings(json& a_json) { a_json["ScreenshotPath"] = screenshotPath; a_json["ApplyCropToScreenshot"] = applyCropToScreenshot; + a_json["HdrPngBitDepth"] = hdrPngBitDepth; + a_json["SdrUsePng"] = sdrUsePng; + a_json["CopyToClipboard"] = copyToClipboard; subrect.SaveSettings(a_json); } void ScreenshotFeature::DrawSettings() { - Util::Text::Disabled("Capture and save run asynchronously - no frame stall."); - Util::Text::Disabled( - "Saves SDR .bmp files. HDR scenes are tonemapped (Reinhard) so the saved\n" - "image matches what's on screen. For true HDR files with HDR10 metadata,\n" - "use Xbox Game Bar (Win+G) or your GPU vendor's overlay (saves .jxr)."); + ImGui::TextWrapped("%s", T(TKEY("async_note"), "Capture and save run asynchronously without stalling the game.")); + + const bool hdrCaptureAvailable = globals::features::hdrDisplay.loaded && + globals::features::hdrDisplay.settings.enableHDR; + + if (hdrCaptureAvailable) { + ImGui::TextWrapped("%s", + T(TKEY("hdr_note"), + "HDR enabled: saves the displayed frame as PNG with HDR10 metadata (48 bpp RGB, cICP/cLLi). " + "Use an HDR-aware viewer such as Windows Photos (HDR on) or Special K SKIF.")); + ImGui::SliderInt( + T(TKEY("hdr_bit_depth"), "HDR PNG bit depth"), + reinterpret_cast(&hdrPngBitDepth), + 7, + 16, + "%d-bit", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text( + "%s", T(TKEY("hdr_bit_depth_tooltip"), + "Quantization for the 48 bpp RGB PNG payload. 11-bit is a good default; " + "higher values increase file size with diminishing returns.")); + + } else { + ImGui::TextWrapped("%s", + T(TKEY("sdr_note"), + "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. " + "SDR captures use the lossless format selected below.")); + } - if (ImGui::Button("Take Screenshot Now")) { - Capture(); + if (ImGui::Button(T(TKEY("take_screenshot"), "Take Screenshot Now"))) { + captureRequested = true; } ImGui::SameLine(); - ImGui::Checkbox("Apply crop", &applyCropToScreenshot); + ImGui::Checkbox(T(TKEY("apply_crop"), "Apply crop"), &applyCropToScreenshot); + + ImGui::SeparatorText(T(TKEY("output"), "Output")); - ImGui::SeparatorText("Output"); + ImGui::Checkbox("Copy saved file to clipboard", ©ToClipboard); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::Text("Places the saved screenshot on the clipboard as a file (paste in Explorer or attach in chat apps)."); + + if (!hdrCaptureAvailable) { + int sdrFormat = sdrUsePng ? 1 : 0; + ImGui::RadioButton("BMP (lossless)", &sdrFormat, 0); + ImGui::SameLine(); + ImGui::RadioButton("PNG (lossless)", &sdrFormat, 1); + sdrUsePng = sdrFormat != 0; + } char buf[260]; strncpy_s(buf, sizeof(buf), screenshotPath.c_str(), _TRUNCATE); - ImGui::PushItemWidth(-FLT_MIN - 120.0f); // leave room for Open button + label + ImGui::PushItemWidth(-FLT_MIN - 120.0f); if (ImGui::InputText("##ScreenshotFolder", buf, sizeof(buf))) { screenshotPath = buf; } @@ -356,38 +660,37 @@ void ScreenshotFeature::DrawSettings() ImGui::SameLine(); const bool canOpen = !screenshotPath.empty(); ImGui::BeginDisabled(!canOpen); - if (ImGui::Button("Open")) { + if (ImGui::Button(T(TKEY("open"), "Open"))) { std::error_code ec; std::filesystem::create_directories(screenshotPath, ec); ShellExecuteA(nullptr, "open", screenshotPath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); } ImGui::EndDisabled(); ImGui::SameLine(); - ImGui::Text("Folder"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip( - "Relative paths resolve against the Skyrim install dir.\n" - "Absolute paths (e.g. D:\\Captures) save there directly."); + ImGui::Text("%s", T(TKEY("folder"), "Folder")); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T(TKEY("folder_tooltip"), + "Relative paths resolve against the Skyrim install dir.\n" + "Absolute paths (e.g. D:\\Captures) save there directly.")); } auto& menuSettings = Menu::GetSingleton()->GetSettings(); Util::InputComboWidget( - "Hotkey", + T(TKEY("hotkey"), "Hotkey"), menuSettings.ScreenshotKey, Menu::GetSingleton()->settingScreenshotKey, "Change##ScreenshotFeature"); if (HotkeyCollidesWithVanilla()) { - Util::Text::Disabled( - "This hotkey collides with vanilla PrintScreen; both saves will fire.\n" - "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a\n" - "different hotkey above."); + Util::Text::WrappedWarning( + T(TKEY("hotkey_collision"), + "This hotkey collides with vanilla PrintScreen; both saves will fire. " + "Set bAllowScreenShot=0 in Skyrim.ini to suppress vanilla, or pick a different hotkey above.")); } - ImGui::SeparatorText("Crop"); + ImGui::SeparatorText(T(TKEY("crop"), "Crop")); - // Preview reflects what Capture() would save. Full source frame so VR users - // can drag-crop across the eye boundary if a seeded preset doesn't fit. + // Preview reflects what Capture() would save. winrt::com_ptr previewTextureKeepAlive; const auto src = SelectCaptureSource(previewTextureKeepAlive); @@ -401,7 +704,7 @@ void ScreenshotFeature::DrawSettings() } } - subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, Util::Subrect::OpaquePreviewBlendCallback); + subrect.DrawEditor(previewView, src.texture, 1.0f, 0.0f, OpaquePreviewBlendCallback); } void ScreenshotFeature::EnsurePreviewCache(ID3D11Texture2D* sourceTexture) @@ -448,6 +751,10 @@ void ScreenshotFeature::EnsurePreviewCache(ID3D11Texture2D* sourceTexture) } void ScreenshotFeature::Reset() +{ +} + +void ScreenshotFeature::ProcessCaptureRequest() { if (captureRequested.exchange(false)) { Capture(); @@ -518,24 +825,26 @@ void ScreenshotFeature::ScreenshotWorkerLoop() continue; } - StripAlphaForBmp(image); - DirectX::ScratchImage convertedImage; - const DirectX::Image* saveImage = PrepareBmpImage(image, convertedImage); - if (!saveImage) { - logger::error("Failed to prepare screenshot image for BMP output."); - continue; - } - Util::FileHelpers::EnsureDirectoryExists(screenshot.outputPath.parent_path()); - HRESULT hr = DirectX::SaveToWICFile( - *saveImage, - DirectX::WIC_FLAGS_NONE, - DirectX::GetWICCodec(DirectX::WIC_CODEC_BMP), - screenshot.outputPath.c_str()); + const bool saveOk = SaveScreenshotToDisk( + image, + screenshot.outputPath, + screenshot.format, + screenshot.hdrPngBitDepth, + screenshot.saveAsHdrPng, + screenshot.saveAsSdrPng); + if (!saveOk) { + logger::error( + "Failed to save {} screenshot.", + screenshot.saveAsHdrPng ? "HDR PNG" : "SDR"); + } - if (FAILED(hr)) { - logger::error("Failed to save screenshot: {:x}", static_cast(hr)); + if (saveOk) { + CopySavedPathToClipboard(screenshot.copyToClipboard, screenshot.outputPath); + } + + if (!saveOk) { ShowInGameNotification("Screenshot failed - see CommunityShaders.log"); } else { logger::info("Saved screenshot to {}", screenshot.outputPath.string()); @@ -548,13 +857,10 @@ void ScreenshotFeature::ScreenshotWorkerLoop() void ScreenshotFeature::ShowInGameNotification(std::string message) { - // ShowHUDMessage must run on the game's main thread; marshall via SKSE's - // task interface. Third arg dedupes spam-clicks - one toast at a time. - if (auto* taskInterface = SKSE::GetTaskInterface()) { - taskInterface->AddTask([msg = std::move(message)]() { - RE::SendHUDMessage::ShowHUDMessage(msg.c_str(), nullptr, true); - }); - } + // ShowHUDMessage must run on the game's main thread. Third arg dedupes spam-clicks. + RunOnMainThread([msg = std::move(message)]() { + RE::SendHUDMessage::ShowHUDMessage(msg.c_str(), nullptr, true); + }); } void ScreenshotFeature::Capture() @@ -619,12 +925,27 @@ void ScreenshotFeature::Capture() context->CopySubresourceRegion(stagingTexture.get(), 0, 0, 0, 0, sourceTexture, 0, &sourceRegion); + // Match SelectCaptureSource: only the flat HDR back-buffer path uses HDR PNG. + // Do not key off DXGI format alone — kFRAMEBUFFER can be float/HDR-sized in SDR mode. + const bool flatHdrCapture = IsFlatHdrScreenshotCapture(); + if (flatHdrCapture && !IsHdrCaptureFormat(srcDesc.Format)) { + logger::error("Unsupported HDR screenshot format: {}", static_cast(srcDesc.Format)); + return; + } + const bool saveAsHdrPng = flatHdrCapture && IsHdrCaptureFormat(srcDesc.Format); + const bool saveAsSdrPng = !saveAsHdrPng && sdrUsePng; + EnsureWorkerThread(); PendingScreenshot screenshot; screenshot.stagingTexture = std::move(stagingTexture); screenshot.format = srcDesc.Format; screenshot.width = copyW; screenshot.height = copyH; - screenshot.outputPath = BuildScreenshotPath(screenshotPath); + screenshot.saveAsHdrPng = saveAsHdrPng; + screenshot.saveAsSdrPng = saveAsSdrPng; + screenshot.hdrPngBitDepth = static_cast(hdrPngBitDepth); + screenshot.outputPath = BuildScreenshotPath(screenshotPath, saveAsHdrPng || saveAsSdrPng); + screenshot.copyToClipboard = copyToClipboard; EnqueueScreenshot(std::move(screenshot)); } +#undef I18N_KEY_PREFIX diff --git a/src/Features/ScreenshotFeature.h b/src/Features/ScreenshotFeature.h index 84e8c11595..6163275cda 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -18,7 +18,8 @@ struct ScreenshotFeature : public Feature virtual std::string GetShortName() override { return "Screenshot"; } virtual std::string_view GetCategory() const override { return FeatureCategories::kUtility; } - virtual bool SupportsVR() override { return true; } + virtual bool SupportsVR() override { return true; } // fork: VR screenshots (per-eye Subrect presets) + virtual bool IsInMenu() const override; virtual void DrawSettings() override; @@ -28,13 +29,15 @@ struct ScreenshotFeature : public Feature virtual void PostPostLoad() override; void Capture(); + // Runs after HDR Present processing so the back buffer matches what's on screen. + void ProcessCaptureRequest(); bool applyCropToScreenshot = true; // Settings std::string screenshotPath = "Screenshots"; // HDR PNG quantization (7-16); used when HDR Display captures the back buffer. unsigned int hdrPngBitDepth = 11; - // SDR / VR output (HDR captures always use PNG). + // SDR output (HDR captures always use PNG). bool sdrUsePng = false; // After save, put the file path on the clipboard (CF_HDROP). bool copyToClipboard = false; diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 51566c5b29..f2d941a7cb 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -12,8 +12,8 @@ #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" -#include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" +#include "Features/ScreenshotFeature.h" #include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Upscaling.h" @@ -268,6 +268,9 @@ struct IDXGISwapChain_Present return func(swapChain, syncInterval, presentFlags); }); + // Runs after HDR Present so the captured back buffer matches what's on screen. + globals::features::screenshotFeature.ProcessCaptureRequest(); + TracyD3D11Collect(globals::state->tracyCtx); return retval; From d36d5abb778969b0cde67feff4acc477454554e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:16:13 +0000 Subject: [PATCH 50/55] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- CMakeLists.txt | 5 +- extern/sk_hdr_png/include/sk_hdr_png.hpp | 2 +- .../DynamicCubemaps/DynamicCubemaps.hlsli | 114 +++++++++--------- package/Shaders/Lighting.hlsl | 2 +- src/CSEditor/EditorWindow.h | 2 +- src/Deferred.cpp | 2 +- src/Features/HDRDisplay.cpp | 6 +- src/Features/InverseSquareLighting.cpp | 2 +- src/Features/SkySync.cpp | 2 - src/Features/SkySync.h | 1 - src/Features/SubsurfaceScattering.cpp | 4 +- src/Globals.cpp | 2 +- src/Menu.cpp | 2 +- src/Menu.h | 34 +++--- src/Menu/CursorLoader.cpp | 8 +- src/Menu/OverlayRenderer.cpp | 2 +- src/Menu/ProfilingRenderer.cpp | 3 +- src/Menu/SettingsTabRenderer.cpp | 8 +- src/Menu/SettingsTabRenderer.h | 8 +- src/Menu/ThemeManager.h | 12 +- 20 files changed, 107 insertions(+), 114 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 03c0f6d499..9ad14faaca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,10 +148,7 @@ target_include_directories( ${EXPRTK_INCLUDE_DIRS} ) -target_compile_definitions( - ${PROJECT_NAME} - PRIVATE SK_HDR_PNG_COMMUNITY_SHADERS -) +target_compile_definitions(${PROJECT_NAME} PRIVATE SK_HDR_PNG_COMMUNITY_SHADERS) target_link_libraries( ${PROJECT_NAME} diff --git a/extern/sk_hdr_png/include/sk_hdr_png.hpp b/extern/sk_hdr_png/include/sk_hdr_png.hpp index b63e5d8600..8e7cd350f1 100644 --- a/extern/sk_hdr_png/include/sk_hdr_png.hpp +++ b/extern/sk_hdr_png/include/sk_hdr_png.hpp @@ -557,7 +557,7 @@ sk_hdr_png::crc32 (const void* typeless_data, size_t offset, size_t len, uint32_ // 1. Remove gAMA chunk (Prevents SKIV from recognizing as HDR) // 2. Remove sRGB chunk (Prevents Discord from rendering in HDR) // -// 3. Add cICP (The primary way of defining HDR10) +// 3. Add cICP (The primary way of defining HDR10) // 4. Add iCCP (Required for Discord to render in HDR) // // (5) Add cLLi [Unnecessary, but probably a good idea] diff --git a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli index e28e7d0e30..c22c603610 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli @@ -48,76 +48,76 @@ namespace DynamicCubemaps if (!useStaticIBL) { # if defined(SKYLIGHTING) - float skylightingSpecular = 0.0; - if (!SharedData::InInterior) { - skylightingSpecular = Skylighting::EvaluateSpecular(skylighting, SphericalHarmonics::FauxSpecularLobe(N, V, roughness)); - } + float skylightingSpecular = 0.0; + if (!SharedData::InInterior) { + skylightingSpecular = Skylighting::EvaluateSpecular(skylighting, SphericalHarmonics::FauxSpecularLobe(N, V, roughness)); + } # endif # if defined(IBL) - if (SharedData::iblSettings.EnableIBL) { - float3 envSample = EnvTexture.SampleLevel(SampColorSampler, R, level); - float3 fullSample = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); - float3 envSpecular = 0.0; - float3 skySpecular = 0.0; - - if (SharedData::iblSettings.DALCMode == 2) { - // Mode 2: DALC-normalized env scaled by DALCAmount + sky overlay - float envLum = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); - envSpecular = Color::IrradianceToLinear((envSample / max(envLum, 0.001)) * directionalAmbientColorSpecular) * SharedData::iblSettings.DALCAmount; - skySpecular = Color::IrradianceToLinear(max(0, fullSample - envSample)) * SharedData::iblSettings.SkyIBLScale; + if (SharedData::iblSettings.EnableIBL) { + float3 envSample = EnvTexture.SampleLevel(SampColorSampler, R, level); + float3 fullSample = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + float3 envSpecular = 0.0; + float3 skySpecular = 0.0; + + if (SharedData::iblSettings.DALCMode == 2) { + // Mode 2: DALC-normalized env scaled by DALCAmount + sky overlay + float envLum = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + envSpecular = Color::IrradianceToLinear((envSample / max(envLum, 0.001)) * directionalAmbientColorSpecular) * SharedData::iblSettings.DALCAmount; + skySpecular = Color::IrradianceToLinear(max(0, fullSample - envSample)) * SharedData::iblSettings.SkyIBLScale; # if defined(SKYLIGHTING) - skySpecular *= skylightingSpecular; + skySpecular *= skylightingSpecular; # endif - } else { - // Mode 0/1: IBL ratio-based - float3 ratio = ImageBasedLighting::GetIBLRatio(); - envSpecular = Color::IrradianceToLinear(envSample * ratio) * SharedData::iblSettings.EnvIBLScale; - skySpecular = Color::IrradianceToLinear(max(0, fullSample - envSample)) * SharedData::iblSettings.SkyIBLScale; + } else { + // Mode 0/1: IBL ratio-based + float3 ratio = ImageBasedLighting::GetIBLRatio(); + envSpecular = Color::IrradianceToLinear(envSample * ratio) * SharedData::iblSettings.EnvIBLScale; + skySpecular = Color::IrradianceToLinear(max(0, fullSample - envSample)) * SharedData::iblSettings.SkyIBLScale; # if defined(SKYLIGHTING) - skySpecular *= skylightingSpecular; + skySpecular *= skylightingSpecular; # endif - } - if (SharedData::InInterior) { - skySpecular = 0; - } + } + if (SharedData::InInterior) { + skySpecular = 0; + } - finalIrradiance = envSpecular + skySpecular; - } else + finalIrradiance = envSpecular + skySpecular; + } else # endif - { - // Fallback without IBL: normalize-by-luminance with DALC + { + // Fallback without IBL: normalize-by-luminance with DALC # if defined(SKYLIGHTING) - if (SharedData::InInterior) { - float3 specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); - float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); - specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; - finalIrradiance = Color::IrradianceToLinear(specularIrradiance); - } else { - float3 specularIrradianceReflections = 0.0; - if (skylightingSpecular > 0.0) { - specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); - float lum = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); - specularIrradianceReflections = (specularIrradianceReflections / max(lum, 0.001)) * directionalAmbientColorSpecular; - specularIrradianceReflections = Color::IrradianceToLinear(specularIrradianceReflections); + if (SharedData::InInterior) { + float3 specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); + float specularIrradianceLuminance = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + finalIrradiance = Color::IrradianceToLinear(specularIrradiance); + } else { + float3 specularIrradianceReflections = 0.0; + if (skylightingSpecular > 0.0) { + specularIrradianceReflections = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + float lum = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); + specularIrradianceReflections = (specularIrradianceReflections / max(lum, 0.001)) * directionalAmbientColorSpecular; + specularIrradianceReflections = Color::IrradianceToLinear(specularIrradianceReflections); + } + float3 specularIrradiance = 0.0; + if (skylightingSpecular < 1.0) { + specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); + float lum = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); + float dalcScaled = Color::IrradianceToGamma(Color::IrradianceToLinear(directionalAmbientColorSpecular) * skylightingSpecular); + specularIrradiance = (specularIrradiance / max(lum, 0.001)) * dalcScaled; + specularIrradiance = Color::IrradianceToLinear(specularIrradiance); + } + finalIrradiance = lerp(specularIrradiance, specularIrradianceReflections, skylightingSpecular); } - float3 specularIrradiance = 0.0; - if (skylightingSpecular < 1.0) { - specularIrradiance = EnvTexture.SampleLevel(SampColorSampler, R, level); - float lum = Color::RGBToLuminance(EnvTexture.SampleLevel(SampColorSampler, R, 15)); - float dalcScaled = Color::IrradianceToGamma(Color::IrradianceToLinear(directionalAmbientColorSpecular) * skylightingSpecular); - specularIrradiance = (specularIrradiance / max(lum, 0.001)) * dalcScaled; - specularIrradiance = Color::IrradianceToLinear(specularIrradiance); - } - finalIrradiance = lerp(specularIrradiance, specularIrradianceReflections, skylightingSpecular); - } # else - float3 specularIrradiance = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); - float specularIrradianceLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); - specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; - finalIrradiance = Color::IrradianceToLinear(specularIrradiance); + float3 specularIrradiance = EnvReflectionsTexture.SampleLevel(SampColorSampler, R, level); + float specularIrradianceLuminance = Color::RGBToLuminance(EnvReflectionsTexture.SampleLevel(SampColorSampler, R, 15)); + specularIrradiance = (specularIrradiance / max(specularIrradianceLuminance, 0.001)) * directionalAmbientColorSpecular; + finalIrradiance = Color::IrradianceToLinear(specularIrradiance); # endif - } + } } else { # if defined(IBL) && defined(LIGHTING) float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index c725747226..65a506be48 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -2214,7 +2214,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPECULAR # endif // SPARKLE -# endif // SNOW +# endif // SNOW # if defined(WORLD_MAP) baseColor.xyz = GetWorldMapBaseColor(rawBaseColor.xyz, baseColor.xyz, projWeight); diff --git a/src/CSEditor/EditorWindow.h b/src/CSEditor/EditorWindow.h index 266a75f961..c4856ef2e7 100644 --- a/src/CSEditor/EditorWindow.h +++ b/src/CSEditor/EditorWindow.h @@ -2,6 +2,7 @@ #include "Buffer.h" +#include "LightEditor.h" #include "Weather/CellLightingWidget.h" #include "Weather/ImageSpaceWidget.h" #include "Weather/LensFlareWidget.h" @@ -10,7 +11,6 @@ #include "Weather/ReferenceEffectWidget.h" #include "Weather/VolumetricLightingWidget.h" #include "Weather/WeatherWidget.h" -#include "LightEditor.h" #include "WeatherUtils.h" #include "Widget.h" diff --git a/src/Deferred.cpp b/src/Deferred.cpp index ab817c33d6..4f0c210b8f 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -6,6 +6,7 @@ #include "State.h" #include "Utils/D3D.h" +#include "Features/CSEditor.h" #include "Features/DynamicCubemaps.h" #include "Features/IBL.h" #include "Features/LightLimitFix/ShadowCasterManager.h" @@ -15,7 +16,6 @@ #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" #include "Features/VR.h" -#include "Features/CSEditor.h" #include "Hooks.h" diff --git a/src/Features/HDRDisplay.cpp b/src/Features/HDRDisplay.cpp index 086129f98e..323aceee28 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -430,11 +430,9 @@ void HDRDisplay::DrawSettings() }; const char* forceEnableLabel = T(TKEY("force_enable_hdr"), "Force Enable HDR"); const char* cancelLabel = T(TKEY("cancel"), "Cancel"); - const float buttonWidth = std::max({ - ThemeManager::Constants::POPUP_BUTTON_WIDTH * Util::GetUIScale(), + const float buttonWidth = std::max({ ThemeManager::Constants::POPUP_BUTTON_WIDTH * Util::GetUIScale(), buttonWidthForLabel(forceEnableLabel), - buttonWidthForLabel(cancelLabel) - }); + buttonWidthForLabel(cancelLabel) }); if (ImGui::Button(forceEnableLabel, ImVec2(buttonWidth, 0))) { { diff --git a/src/Features/InverseSquareLighting.cpp b/src/Features/InverseSquareLighting.cpp index 946f841afa..61fc903c41 100644 --- a/src/Features/InverseSquareLighting.cpp +++ b/src/Features/InverseSquareLighting.cpp @@ -1,8 +1,8 @@ #include "InverseSquareLighting.h" +#include "CSEditor/EditorWindow.h" #include "Features/InverseSquareLighting/Common.h" #include "Features/InverseSquareLighting/RadiusMath.h" #include "LightLimitFix.h" -#include "CSEditor/EditorWindow.h" #include void InverseSquareLighting::PostPostLoad() diff --git a/src/Features/SkySync.cpp b/src/Features/SkySync.cpp index 0c4dbcf188..81063d16f6 100644 --- a/src/Features/SkySync.cpp +++ b/src/Features/SkySync.cpp @@ -558,6 +558,4 @@ inline void SkySync::ShadowFader::ClampDirection(RE::NiPoint3& dir) dir.z = sinElev; } - - #undef I18N_KEY_PREFIX diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 53bd73727a..52c7dd73d1 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -63,7 +63,6 @@ struct SkySync : Feature static inline REL::Relocation func; }; - private: enum class CellFlagExt : uint16_t { diff --git a/src/Features/SubsurfaceScattering.cpp b/src/Features/SubsurfaceScattering.cpp index 591d1ae194..3333e82a07 100644 --- a/src/Features/SubsurfaceScattering.cpp +++ b/src/Features/SubsurfaceScattering.cpp @@ -239,9 +239,7 @@ void SubsurfaceScattering::DrawSSS() blurCBData.BurleySamples = settings.BurleySamples; // Burley always does full albedo removal/reapply; scatter mode only applies to Separable SSS. - blurCBData.ScatterMode = (settings.SSMode == 0) - ? (uint)std::clamp(settings.ScatterMode, (int)kPreScatter, (int)kPreAndPostScatter) - : (uint)kPostScatter; + blurCBData.ScatterMode = (settings.SSMode == 0) ? (uint)std::clamp(settings.ScatterMode, (int)kPreScatter, (int)kPreAndPostScatter) : (uint)kPostScatter; blurCBData.MeanFreePathBase = settings.MeanFreePathBase; blurCBData.MeanFreePathHuman = settings.MeanFreePathHuman; diff --git a/src/Globals.cpp b/src/Globals.cpp index aebf343baa..dd997a377a 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -1,6 +1,7 @@ #include "Globals.h" #include "Deferred.h" +#include "Features/CSEditor.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" #include "Features/ExponentialHeightFog.h" @@ -36,7 +37,6 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/CSEditor.h" #include "Features/WetnessEffects.h" #include "Menu.h" #include "ShaderCache.h" diff --git a/src/Menu.cpp b/src/Menu.cpp index 2fa61aeb7b..a14f02dbd9 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -27,10 +27,10 @@ #include "I18n/I18n.h" #include "Menu/AdvancedSettingsRenderer.h" #include "Menu/BackgroundBlur.h" +#include "Menu/CursorLoader.h" #include "Menu/FeatureListRenderer.h" #include "Menu/Fonts.h" #include "Menu/HomePageRenderer.h" -#include "Menu/CursorLoader.h" #include "Menu/IconLoader.h" #include "Menu/MenuHeaderRenderer.h" #include "Menu/OverlayRenderer.h" diff --git a/src/Menu.h b/src/Menu.h index e610271f31..609c29b5dd 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -178,10 +178,10 @@ class Menu bool settingSkipCompilationKey = false; bool settingsEffectsToggle = false; bool settingOverlayToggleKey = false; - bool settingShaderBlockPrevKey = false; // Debug: capture shader block prev key - bool settingShaderBlockNextKey = false; // Debug: capture shader block next key - bool settingCSEditorToggleKey = false; // CS Editor toggle key - bool settingScreenshotKey = false; // Screenshot capture key + bool settingShaderBlockPrevKey = false; // Debug: capture shader block prev key + bool settingShaderBlockNextKey = false; // Debug: capture shader block next key + bool settingCSEditorToggleKey = false; // CS Editor toggle key + bool settingScreenshotKey = false; // Screenshot capture key // Font caching (made public for ThemeManager and OverlayRenderer access) // Marked mutable because they're cache fields that may be updated from const methods @@ -313,7 +313,7 @@ class Menu bool CenterHeader = false; // whether to center the header title and logo float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds bool BackgroundBlurEnabled = true; // enable background blur effect - bool UseCustomCursor = false; // use theme cursor images instead of default ImGui cursors + bool UseCustomCursor = false; // use theme cursor images instead of default ImGui cursors struct CursorImageSettings { std::string File; @@ -467,19 +467,19 @@ class Menu { std::vector ToggleKey = { InputCombo::Keyboard(VK_END) }; std::vector SkipCompilationKey = { InputCombo::Keyboard(VK_ESCAPE) }; - std::vector EffectToggleKey = { InputCombo::Keyboard(VK_MULTIPLY) }; // toggle all effects - std::vector OverlayToggleKey = { InputCombo::Keyboard(VK_F10) }; // Global overlay toggle key for all overlays - std::vector ShaderBlockPrevKey = { InputCombo::Keyboard(VK_PRIOR) }; // Debug: cycle backward through shaders (PageUp) - std::vector ShaderBlockNextKey = { InputCombo::Keyboard(VK_NEXT) }; // Debug: cycle forward through shaders (PageDown) + std::vector EffectToggleKey = { InputCombo::Keyboard(VK_MULTIPLY) }; // toggle all effects + std::vector OverlayToggleKey = { InputCombo::Keyboard(VK_F10) }; // Global overlay toggle key for all overlays + std::vector ShaderBlockPrevKey = { InputCombo::Keyboard(VK_PRIOR) }; // Debug: cycle backward through shaders (PageUp) + std::vector ShaderBlockNextKey = { InputCombo::Keyboard(VK_NEXT) }; // Debug: cycle forward through shaders (PageDown) std::vector CSEditorToggleKey = { InputCombo::Keyboard(VK_SHIFT), InputCombo::Keyboard(VK_END) }; // CS Editor toggle key - std::vector ScreenshotKey = { InputCombo::Keyboard(VK_SNAPSHOT) }; // Screenshot capture key - bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging - bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed - bool SkipClearCacheConfirmation = false; // Skip confirmation dialog when clearing shader cache - bool AutoHideFeatureList = false; // Auto-hide left feature list panel, show on hover - bool SkipConstraintWarning = false; // Skip popup when a setting change creates new constraints - bool RequireShiftToDock = true; // Require holding Shift to dock windows - bool UseResolutionFont = true; // When true, runtime font size scales with screen resolution; when persisted to theme files, FontSize is zeroed for backward compatibility + std::vector ScreenshotKey = { InputCombo::Keyboard(VK_SNAPSHOT) }; // Screenshot capture key + bool EnableShaderBlocking = false; // Enable shader blocking hotkeys for debugging + bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed + bool SkipClearCacheConfirmation = false; // Skip confirmation dialog when clearing shader cache + bool AutoHideFeatureList = false; // Auto-hide left feature list panel, show on hover + bool SkipConstraintWarning = false; // Skip popup when a setting change creates new constraints + bool RequireShiftToDock = true; // Require holding Shift to dock windows + bool UseResolutionFont = true; // When true, runtime font size scales with screen resolution; when persisted to theme files, FontSize is zeroed for backward compatibility ThemeSettings Theme; std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; diff --git a/src/Menu/CursorLoader.cpp b/src/Menu/CursorLoader.cpp index 2ae20449a5..dae9b42716 100644 --- a/src/Menu/CursorLoader.cpp +++ b/src/Menu/CursorLoader.cpp @@ -28,7 +28,11 @@ namespace Util::CursorLoader void ForEachSlot(const Menu::ThemeSettings& theme, auto&& fn) { - static constexpr struct { ImGuiMouseCursor cursor; const char* defaultFile; } kSlots[] = { + static constexpr struct + { + ImGuiMouseCursor cursor; + const char* defaultFile; + } kSlots[] = { { ImGuiMouseCursor_Arrow, "cursor.png" }, { ImGuiMouseCursor_TextInput, "cursor_text.png" }, { ImGuiMouseCursor_ResizeAll, "cursor_resize_all.png" }, @@ -68,7 +72,7 @@ namespace Util::CursorLoader bool IsPathAllowed(const std::filesystem::path& path) { return Util::IsPathWithinDirectory(Util::PathHelpers::GetThemesPath(), path) || - Util::IsPathWithinDirectory(Util::PathHelpers::GetCursorsPath(), path); + Util::IsPathWithinDirectory(Util::PathHelpers::GetCursorsPath(), path); } } diff --git a/src/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index 5a5a45b418..416c587e9c 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -17,9 +17,9 @@ #include "Globals.h" #include "I18n/I18n.h" #include "Menu.h" +#include "Menu/CursorLoader.h" #include "ShaderCache.h" #include "State.h" -#include "Menu/CursorLoader.h" #include "Util.h" #include "Features/PerformanceOverlay.h" diff --git a/src/Menu/ProfilingRenderer.cpp b/src/Menu/ProfilingRenderer.cpp index 60ede4e6a5..98ab52fe46 100644 --- a/src/Menu/ProfilingRenderer.cpp +++ b/src/Menu/ProfilingRenderer.cpp @@ -2,8 +2,8 @@ #include #include -#include #include +#include #include "Globals.h" #include "I18n/I18n.h" @@ -340,7 +340,6 @@ void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) ImGui::EndTable(); } } - } void ProfilingRenderer::RenderFeatureTimers(const std::string& featurePrefix) diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index c20cff5cd2..d4f8961094 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -6,12 +6,12 @@ #include #include "BackgroundBlur.h" +#include "CursorLoader.h" #include "Features/ScreenshotFeature.h" #include "Features/VR.h" #include "Fonts.h" #include "Globals.h" #include "I18n/I18n.h" -#include "CursorLoader.h" #include "IconLoader.h" #include "Menu.h" #include "ShaderCache.h" @@ -522,9 +522,9 @@ void SettingsTabRenderer::RenderBehaviorTab() } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("%s", T("menu.settings.use_custom_cursor_tooltip", - "Loads cursor PNGs from the active theme folder (Themes//).\n" - "Supported files include cursor.png (arrow), cursor_text.png (typing), cursor_resize_ew.png, cursor_resize_ns.png, and more.\n" - "Missing types fall back to the default ImGui cursor. Configure per-type hotspots in theme JSON.")); + "Loads cursor PNGs from the active theme folder (Themes//).\n" + "Supported files include cursor.png (arrow), cursor_text.png (typing), cursor_resize_ew.png, cursor_resize_ns.png, and more.\n" + "Missing types fall back to the default ImGui cursor. Configure per-type hotspots in theme JSON.")); } if (themeSettings.UseCustomCursor) { diff --git a/src/Menu/SettingsTabRenderer.h b/src/Menu/SettingsTabRenderer.h index b7cea06884..ceb6244b05 100644 --- a/src/Menu/SettingsTabRenderer.h +++ b/src/Menu/SettingsTabRenderer.h @@ -17,10 +17,10 @@ class SettingsTabRenderer bool& settingsEffectsToggle; bool& settingSkipCompilationKey; bool& settingOverlayToggleKey; - bool& settingShaderBlockPrevKey; // Debug: shader block previous key - bool& settingShaderBlockNextKey; // Debug: shader block next key - bool& settingCSEditorToggleKey; // CS Editor toggle key - bool& settingScreenshotKey; // Screenshot capture key + bool& settingShaderBlockPrevKey; // Debug: shader block previous key + bool& settingShaderBlockNextKey; // Debug: shader block next key + bool& settingCSEditorToggleKey; // CS Editor toggle key + bool& settingScreenshotKey; // Screenshot capture key }; static void RenderGeneralSettings( diff --git a/src/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 212a5ac400..445bae36a0 100644 --- a/src/Menu/ThemeManager.h +++ b/src/Menu/ThemeManager.h @@ -209,12 +209,12 @@ class ThemeManager // Search input constants static constexpr float SEARCH_BASELINE_SCREEN_HEIGHT = 1440.0f; // Search chrome is authored for 2K. - static constexpr float SEARCH_ICON_SIZE = 20.0f; // Default search icon size - static constexpr float SEARCH_ICON_ALPHA = 0.7f; // Default search icon opacity - static constexpr float SEARCH_ICON_OFFSET_X = 8.0f; // Search icon offset from input edge - static constexpr float SEARCH_INPUT_PADDING_EXTRA = 14.0f; // Extra input padding after icon - static constexpr float SEARCH_INPUT_FRAME_PADDING_Y = 6.0f; // Search input vertical padding - static constexpr float SEARCH_ICON_STROKE_RATIO = 0.11f; // Search icon stroke thickness relative to size + static constexpr float SEARCH_ICON_SIZE = 20.0f; // Default search icon size + static constexpr float SEARCH_ICON_ALPHA = 0.7f; // Default search icon opacity + static constexpr float SEARCH_ICON_OFFSET_X = 8.0f; // Search icon offset from input edge + static constexpr float SEARCH_INPUT_PADDING_EXTRA = 14.0f; // Extra input padding after icon + static constexpr float SEARCH_INPUT_FRAME_PADDING_Y = 6.0f; // Search input vertical padding + static constexpr float SEARCH_ICON_STROKE_RATIO = 0.11f; // Search icon stroke thickness relative to size static constexpr float SEARCH_ICON_HANDLE_STROKE_RATIO = 0.105f; static constexpr float COMBO_SEARCH_ICON_SIZE = 16.0f; // Icon size for search inside combos static constexpr float COMBO_SEARCH_ICON_ALPHA = 0.5f; // Icon alpha for subtle appearance From a58e2df3316cd6c7101221bfdc70363be640238f Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 19:21:52 -0700 Subject: [PATCH 51/55] chore(i18n): regen en.json; dedup snow RT comment Run tools/extract-i18n.py --write so en.json matches the strings actually referenced via T() in source -- the i18n PR check (extract-i18n.py --check) was failing. The removed keys are fork DrawSettings (LLF, SSGI, VL, RenderDoc, DynamicCubemaps, VR, profiling UI) that render in plain English and are not yet wrapped in T(); they return when wrapped (tracked as the i18n follow-up). T() falls back to the inline English default at runtime, so no behavior change. Also remove a duplicated kSNOW/kSNOW_SWAP comment block in Hooks.cpp (a keep-ours/adopt-theirs merge artifact; comment only). --- .../CommunityShaders/Translations/en.json | 184 +----------------- src/Hooks.cpp | 3 - 2 files changed, 5 insertions(+), 182 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json index 4d6b3de2dd..e0888b1004 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/en.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -546,14 +546,12 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = Tailwind (wind behind player)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = Crosswind (left/right)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = Headwind (wind coming toward player)", - "feature.dynamic_cubemaps.advanced_vr_settings": "Advanced VR Settings", "feature.dynamic_cubemaps.color": "Color", "feature.dynamic_cubemaps.creator_info": "You must enable creator mode by adding the shader define CREATOR", "feature.dynamic_cubemaps.description": "Provides real-time environment mapping and reflections by generating dynamic cube maps that capture the surrounding environment, enabling realistic reflections on surfaces.", "feature.dynamic_cubemaps.dynamic_cubemap_creator": "Dynamic Cubemap Creator", "feature.dynamic_cubemaps.enable_creator": "Enable Creator", "feature.dynamic_cubemaps.enable_ssr": "Enable Screen Space Reflections", - "feature.dynamic_cubemaps.enable_ssr_tooltip": "Enable Screen Space Reflections on Water", "feature.dynamic_cubemaps.export": "Export", "feature.dynamic_cubemaps.key_feature_1": "Real-time environment capture for realistic reflections", "feature.dynamic_cubemaps.key_feature_2": "Dynamic cube map generation based on camera position", @@ -563,7 +561,6 @@ "feature.dynamic_cubemaps.name": "Dynamic Cubemaps", "feature.dynamic_cubemaps.roughness": "Roughness", "feature.dynamic_cubemaps.screen_space_reflections": "Screen Space Reflections", - "feature.dynamic_cubemaps.vr_restart_required": "A restart is required to enable in VR. Save Settings after enabling and restart the game.", "feature.exp_height_fog.apply_vanilla_fade": "Apply Vanilla Fade", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "Applies vanilla fade brightness to exponential height fog.", "feature.exp_height_fog.cubemap_mip_level": "Cubemap Mip Level", @@ -684,7 +681,7 @@ "feature.grass_lighting.lighting": "Lighting", "feature.grass_lighting.name": "Grass Lighting", "feature.grass_lighting.override_complex": "Override Complex Grass Lighting Settings", - "feature.grass_lighting.override_complex_tooltip": "Override the settings set by the grass mesh author. Complex grass authors can define the brightness for their grass meshes. However, some authors may not account for the extra lights available from Community Shaders. This option will treat their grass settings like non-complex grass. This was the default in Community Shaders < 0.7.0", + "feature.grass_lighting.override_complex_tooltip": "Override the settings set by the grass mesh author. Complex grass authors can define the brightness for their grass meshes. However, some authors may not account for the extra lights available from Open Shaders. This option will treat their grass settings like non-complex grass. This was the default in Community Shaders < 0.7.0", "feature.grass_lighting.specular_desc": "Specular highlights for complex grass", "feature.grass_lighting.specular_strength": "Specular Strength", "feature.grass_lighting.specular_strength_tooltip": "Specular highlight strength.", @@ -850,21 +847,7 @@ "feature.light_editor.sort_by": "Sort By", "feature.light_editor.spotlight_not_applicable": "Spotlight: ISL light type flags not applicable", "feature.light_editor.total_lights": "Total Lights: %u", - "feature.light_limit_fix.debug": "Debug", - "feature.light_limit_fix.debug_feature_enabled": "DEBUG FEATURE - LIGHT LIMIT VISUALISATION ENABLED", - "feature.light_limit_fix.description": "Light Limit Fix removes the vanilla game's 4-light limit, allowing unlimited dynamic lights in scenes.\nThis dramatically improves lighting quality and enables more realistic illumination scenarios.", - "feature.light_limit_fix.enable_lights_vis": "Enable Lights Visualisation", - "feature.light_limit_fix.enable_lights_vis_tooltip": "Enables visualization of the light limit\n", - "feature.light_limit_fix.key_feature_1": "Removes 4-light limit", - "feature.light_limit_fix.key_feature_2": "Unlimited dynamic lights", - "feature.light_limit_fix.key_feature_3": "Improved lighting quality", - "feature.light_limit_fix.key_feature_4": "Enhanced visual realism", - "feature.light_limit_fix.key_feature_5": "Enhanced visual realism", - "feature.light_limit_fix.light_limit_vis": "Light Limit Visualization", - "feature.light_limit_fix.lights_vis_mode": "Lights Visualisation Mode", - "feature.light_limit_fix.lights_vis_mode_tooltip": " - Visualise the light limit. Red when the \"strict\" light limit is reached (portal-strict lights).\n - Visualise the number of strict lights.\n - Visualise the number of clustered lights.\n - Visualize the Shadow Mask.\n", "feature.light_limit_fix.name": "Light Limit Fix", - "feature.light_limit_fix.statistics": "Statistics", "feature.linear_lighting.ambient_gamma": "Ambient Gamma", "feature.linear_lighting.ambient_multiplier": "Ambient Multiplier", "feature.linear_lighting.blood_effects_multiplier": "Blood Effects Multiplier", @@ -984,17 +967,12 @@ "feature.renderdoc.disk_usage": "Disk Usage", "feature.renderdoc.disk_usage_tooltip": "Monitor capture storage usage", "feature.renderdoc.double_click_hint": "Double-click a filename to open the capture file", - "feature.renderdoc.enable_capture": "Enable RenderDoc Capture", - "feature.renderdoc.enable_capture_tooltip": "Enable RenderDoc frame capture for providing debug captures to the Community Shaders team.", - "feature.renderdoc.enable_capture_tooltip2": "Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled.", "feature.renderdoc.hover_hint": "Hover over filenames for file details", "feature.renderdoc.no_files": "No capture files found.", "feature.renderdoc.not_enough_space": "Not enough free disk space to create a capture.", "feature.renderdoc.ok": "OK", "feature.renderdoc.open_capture_dir": "Open Capture Directory", "feature.renderdoc.refresh_list": "Refresh List", - "feature.renderdoc.restart_to_disable": "Requires restart to disable RenderDoc capture, performance will be severely impacted.", - "feature.renderdoc.restart_to_enable": "Requires restart to enable RenderDoc capture.", "feature.renderdoc.space_required": "At least {} MB of free space is required.", "feature.renderdoc.yes_delete": "Yes, Delete All", "feature.screen_space_gi.ao_only": "AO only", @@ -1008,7 +986,6 @@ "feature.screen_space_gi.denoising": "Denoising", "feature.screen_space_gi.depth_fade_range": "Depth Fade Range", "feature.screen_space_gi.depth_fade_range_tooltip": "Distance range where depth-based effects fade out.", - "feature.screen_space_gi.description": "Screen Space Global Illumination adds realistic indirect lighting and ambient occlusion to the game. This technique simulates how light bounces off surfaces to illuminate other objects naturally.", "feature.screen_space_gi.enabled": "Enabled", "feature.screen_space_gi.enabled_tooltip": "Enable Screen Space Global Illumination. When disabled, all other settings are ignored.", "feature.screen_space_gi.extreme": "Extreme", @@ -1026,11 +1003,6 @@ "feature.screen_space_gi.il_saturation": "IL Saturation", "feature.screen_space_gi.il_source_brightness": "IL Source Brightness", "feature.screen_space_gi.indirect_lighting": "Indirect Lighting (IL)", - "feature.screen_space_gi.key_feature_1": "Realistic indirect lighting", - "feature.screen_space_gi.key_feature_2": "Enhanced ambient occlusion", - "feature.screen_space_gi.key_feature_3": "Improved visual depth and atmosphere", - "feature.screen_space_gi.key_feature_4": "Temporal denoising for smooth results", - "feature.screen_space_gi.key_feature_5": "Configurable quality and performance settings", "feature.screen_space_gi.low": "Low", "feature.screen_space_gi.low_tooltip": "Quarter res and blurry.", "feature.screen_space_gi.max_frame_accumulation": "Max Frame Accumulation", @@ -1062,7 +1034,6 @@ "feature.screen_space_gi.view_resize": "View Resize", "feature.screen_space_gi.visual": "Visual", "feature.screen_space_gi.visual_il": "Visual - IL", - "feature.screen_space_gi.vr_warning": "\n\nWarning: In VR, this feature may have visual artifacts and can have a significant performance impact due to the nature of screen space effects.", "feature.screen_space_shadows.bilinear_threshold": "Bilinear Threshold", "feature.screen_space_shadows.bilinear_threshold_tooltip": "Depth threshold for edge detection during bilinear interpolation. Higher values smooth more aggressively across edges.", "feature.screen_space_shadows.description": "Screen Space Shadows enhances shadow quality by adding detailed contact shadows and improving shadow accuracy.\nThis technique adds fine-detail shadows that traditional shadow mapping might miss.", @@ -1096,7 +1067,7 @@ "feature.screenshot.name": "Screenshot", "feature.screenshot.open": "Open", "feature.screenshot.output": "Output", - "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR and VR captures use the lossless format selected below.", + "feature.screenshot.sdr_note": "Enable HDR Display to capture HDR PNG screenshots with HDR10 metadata. SDR captures use the lossless format selected below.", "feature.screenshot.take_screenshot": "Take Screenshot Now", "feature.skin.a_multiplier_for_the_vanilla_specular_map_applied": "A multiplier for the vanilla specular map, applied to the first layer's roughness", "feature.skin.adds_a_constant_layer_of_wetness_to_all": "Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort.", @@ -1323,7 +1294,6 @@ "feature.unified_water.regenerate_flowmap": "Regenerate Flowmap", "feature.unified_water.use_optimised_meshes": "Use Optimised Meshes", "feature.unified_water.use_optimised_meshes_tooltip": "Uses meshes with significantly lower tri-count for improved performance with no visual quality loss.\nWill only affect newly created water - requires a change of location or game restart to take effect.", - "feature.upscaling.backend_diagnostics": "Backend Diagnostics", "feature.upscaling.description": "Advanced upscaling and frame generation technologies for improved performance", "feature.upscaling.dlss_model_preset": "DLSS Model Preset", "feature.upscaling.dlss_model_preset_default": "Default", @@ -1331,21 +1301,9 @@ "feature.upscaling.dlss_model_preset_k": "Preset K", "feature.upscaling.dlss_model_preset_l": "Preset L", "feature.upscaling.dlss_model_preset_m": "Preset M", - "feature.upscaling.dlss_model_preset_tooltip": "Choose which DLSS AI model preset to use.\nEach model offers different visual quality, performance, and motion stability.\nSet to 'Default' for automatic selection based on your Upscale Preset and hardware.\nChanging this setting requires a restart to take effect.", - "feature.upscaling.force_enable_frame_generation": "Force Enable Frame Generation", "feature.upscaling.fps_limit": "FPS Limit", "feature.upscaling.fps_limit_tooltip_1": "Set your frame cap target.", "feature.upscaling.fps_limit_tooltip_2": "Start about 2-3 FPS below refresh rate (e.g. 117 for 120 Hz).", - "feature.upscaling.frame_generation": "Frame Generation", - "feature.upscaling.frame_generation_available": "AMD FSR Frame Generation is available.", - "feature.upscaling.frame_generation_desc": "Frame Generation interpolates real frames with generated ones for a smoother experience", - "feature.upscaling.frame_generation_in_menus": "Frame Generation in Menus", - "feature.upscaling.frame_generation_in_menus_tooltip_1": "Keeps frame generation active while game menus are open.", - "feature.upscaling.frame_generation_in_menus_tooltip_2": "May feel smoother, but increases menu input latency.", - "feature.upscaling.frame_generation_proxy_note": "Requires a D3D11 to D3D12 proxy which can create compatibility issues", - "feature.upscaling.frame_generation_restart_note": "Toggling this setting requires a restart to work correctly", - "feature.upscaling.frame_generation_tech": "Uses AMD FSR Frame Generation technology", - "feature.upscaling.frame_limit_vrr": "Frame Limit (Variable Refresh Rate)", "feature.upscaling.key_feature_1": "DLSS (Deep Learning Super Sampling) support", "feature.upscaling.key_feature_2": "FSR (FidelityFX Super Resolution) support", "feature.upscaling.key_feature_3": "TAA (Temporal Anti-Aliasing) support", @@ -1357,7 +1315,6 @@ "feature.upscaling.low_latency_mode_tooltip_1": "Cuts input delay by syncing CPU work closer to the GPU.", "feature.upscaling.low_latency_mode_tooltip_2": "Can reduce max FPS a little, but usually feels more responsive.", "feature.upscaling.marker_optimization_unavailable": "Marker optimization unavailable (PCL not loaded).", - "feature.upscaling.method": "Method", "feature.upscaling.method_none": "None", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "Upscaling", @@ -1373,9 +1330,6 @@ "feature.upscaling.reflex_not_available": "Reflex is not available. Ensure sl.reflex.dll is present and restart.", "feature.upscaling.sharpness": "Sharpness", "feature.upscaling.streamline_logging": "Streamline Logging", - "feature.upscaling.streamline_logging_restart_note": "Changing this requires a restart to take effect.", - "feature.upscaling.streamline_logging_tooltip": "Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G.", - "feature.upscaling.upscale_preset": "Upscale Preset", "feature.upscaling.upscaling_intermediates": "Upscaling Intermediates", "feature.upscaling.use_fps_limit": "Use FPS Limit", "feature.upscaling.use_fps_limit_tooltip_1": "Uses Reflex's internal FPS cap for steadier frametimes.", @@ -1386,7 +1340,6 @@ "feature.upscaling.view_resize": "View Resize", "feature.upscaling.vr_intermediates_not_created": "VR intermediates not yet created (enter game world)", "feature.volumetric_lighting.description": "Volumetric Lighting creates realistic light scattering effects through fog, dust, and atmospheric particles.\nThis adds dramatic god rays and atmospheric depth to both interior and exterior environments.", - "feature.volumetric_lighting.enable_exteriors": "Enable Volumetric Lighting in Exteriors", "feature.volumetric_lighting.enable_interiors": "Enable Volumetric Lighting in Interiors", "feature.volumetric_lighting.exterior_depth": "Exterior Depth", "feature.volumetric_lighting.exterior_height": "Exterior Height", @@ -1412,31 +1365,6 @@ "feature.volumetric_shadows.key_feature_3": "Multi-cascade support", "feature.volumetric_shadows.key_feature_4": "Optimized for effects rendering", "feature.volumetric_shadows.name": "Volumetric Shadows", - "feature.vr.description": "Provides VR-specific optimizations and enhancements for Community Shaders, improving performance and visual quality in virtual reality environments.", - "feature.vr.key_feature_1": "Depth buffer culling optimization for VR performance", - "feature.vr.key_feature_2": "In-scene overlay menu with HMD/Controller/Fixed World attach modes", - "feature.vr.key_feature_3": "VR controller input with customizable button mappings", - "feature.vr.key_feature_4": "Grip-to-drag overlay positioning with depth control", - "feature.vr.key_feature_5": "Configurable occlusion culling parameters", - "feature.vr.key_feature_6": "Enhanced VR compatibility with SteamVR and OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "Debug", - "feature.vr_stereo.debug_pom_depth": "Debug POM Depth", - "feature.vr_stereo.disocclusion_depth_threshold": "Disocclusion Depth Threshold", - "feature.vr_stereo.enable": "Enable", - "feature.vr_stereo.enable_stereo_reprojection": "Enable Stereo Reprojection", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "Reprojects Eye 0 (left) pixels into Eye 1 (right) using depth and motion data,\nskipping redundant full shading where the views overlap.\nReduces GPU cost in VR by shading each pixel fewer times per frame.", - "feature.vr_stereo.forward_occlusion_scale": "Forward Occlusion Scale", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "Prevents Eye 0 silhouette edges from bleeding onto Eye 1 backgrounds.\nFires when Eye 0 depth is within this fraction of Eye 1 depth (e.g. 0.5 = Eye 0 less than 2x Eye 1 depth).\nLower = more aggressive. 0 = disabled.", - "feature.vr_stereo.full_blend_depth_view": "Full Blend Depth View", - "feature.vr_stereo.full_blend_distance": "Full Blend Distance", - "feature.vr_stereo.full_blend_distance_tooltip": "Geometry closer than this distance (game units) is fully shaded in both eyes and bilaterally blended for 2x supersampling. 0 = disabled.", - "feature.vr_stereo.full_blend_zone_hint": " Cyan = full blend zone (closer = stronger tint)", - "feature.vr_stereo.off": "Off", - "feature.vr_stereo.pom_depth_scale": "POM Depth Scale", - "feature.vr_stereo.pom_depth_scale_tooltip": "Scale factor for POM depth correction in stereo reprojection.\n1.0 = physical scale. Increase for more visible POM stereo depth.", - "feature.vr_stereo.restart_required": "Restart is required to enable VR stereo reprojection.", - "feature.vr_stereo.skip_pixel_reprojection": "Skip Pixel Reprojection", "feature.water_effects.description": "Water Effects enhances water rendering with realistic caustics and underwater lighting effects.\nThis feature adds dynamic light patterns and improved water visual quality.", "feature.water_effects.key_feature_1": "Realistic water caustics", "feature.water_effects.key_feature_2": "Enhanced underwater lighting", @@ -1580,16 +1508,7 @@ "feature.wetness_effects.weather_transition_speed_tooltip": "How fast wetness appears when raining and how quickly it dries after rain has stopped.", "feature.wetness_effects.wetness_effects": "Wetness Effects", "feature.wetness_effects.wetness_in_exterior": "Wetness In/Exterior", - "menu.advanced.active_shaders": "Active Shaders", "menu.advanced.active_shaders_tooltip": "List of shaders that have been used in recent frames. Enable Shader Blocking above to use hotkeys to cycle through and block shaders for debugging. Shaders not used for ~1 second are removed from this list.", - "menu.advanced.active_shaders_used_recently": "Active Shaders (Used Recently)", - "menu.advanced.addresses": "Addresses", - "menu.advanced.avg_parallelism_metric": "Average parallelism (W/S): %.2fx", - "menu.advanced.avg_parallelism_tooltip_1": "Average useful concurrency in this workload.", - "menu.advanced.avg_parallelism_tooltip_2": "Roughly the worker count where adding more cores gives diminishing returns.", - "menu.advanced.avoid_flow_control": "Avoid Flow Control", - "menu.advanced.avoid_flow_control_tooltip": "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\nForces fxc to flatten branches into predicated ops rather than emitting dynamic flow control. Often a win for short branch bodies and uniformly-taken branches; usually a loss for long divergent branches that vanilla flow control would skip entirely.\nResets every launch. Toggling this clears the shader cache and triggers a full recompile.", - "menu.advanced.background_compiler_threads": "Background Compiler Threads", "menu.advanced.background_compiler_threads_tooltip": "Number of threads used to compile shaders during gameplay. Defaults to half of performance cores to avoid impacting the render thread. Higher values finish compilation faster but may cause stuttering.", "menu.advanced.block_next": "Block Next:", "menu.advanced.block_previous": "Block Previous:", @@ -1610,109 +1529,41 @@ "menu.advanced.column_key_tooltip": "Shader key", "menu.advanced.column_type": "Type", "menu.advanced.column_type_tooltip": "Shader type", - "menu.advanced.compiler_threads": "Compiler Threads", "menu.advanced.compiler_threads_tooltip": "Number of threads used to compile shaders at startup. Defaults to all logical cores minus one for OS headroom (E-cores included). Higher values finish compilation faster but may make the system less responsive.", "menu.advanced.compute": "Compute", "menu.advanced.compute_tooltip": "Replace Compute Shaders. When false, will disable the custom Compute Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", "menu.advanced.copy_info": "Copy Info", "menu.advanced.copy_info_tooltip": "Copy complete shader information including cache path to clipboard", - "menu.advanced.copy_key": "Copy key", - "menu.advanced.dump_ini_settings": "Dump Ini Settings", "menu.advanced.dump_shaders": "Dump Shaders", "menu.advanced.dump_shaders_tooltip": "Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this.", - "menu.advanced.efficiency_progress": "{:.1f}% efficient / {:.1f}% gap", - "menu.advanced.enable_file_watcher": "Enable File Watcher", - "menu.advanced.enable_file_watcher_tooltip": "Automatically recompile shaders on file change. Intended for developing.", "menu.advanced.enable_shader_blocking": "Enable Shader Blocking", "menu.advanced.enable_shader_blocking_tooltip": "Enables hotkeys to cycle through and block individual shaders for debugging purposes.", - "menu.advanced.frame_annotations": "Frame Annotations", - "menu.advanced.frame_annotations_tooltip": "Enable detailed frame annotations for debugging render passes and draw calls.", - "menu.advanced.half_precision": "Half Precision (Partial Precision)", - "menu.advanced.half_precision_tooltip": "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\nLets fxc downgrade unmarked float ops to FP16 where it can prove safety, on top of the existing min16float type hints.\nOn FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register pressure and double ALU throughput, but it can also introduce minor visual differences in shaders that haven't been audited for precision sensitivity.\nToggling this clears the shader cache and triggers a full recompile.", - "menu.advanced.infinite_core_efficiency": "Infinite-core efficiency", - "menu.advanced.infinite_core_efficiency_metric": "Infinite-core efficiency (S/T_p): %.1f%%", - "menu.advanced.infinite_core_efficiency_tooltip_1": "How close runtime is to the infinite-core lower bound.", - "menu.advanced.infinite_core_efficiency_tooltip_2": "100%% means T_p == S.", - "menu.advanced.infinite_core_gap_metric": "Infinite-core gap: %.1f%%", - "menu.advanced.infinite_core_gap_tooltip_1": "Distance from ideal infinite-core time.", - "menu.advanced.infinite_core_gap_tooltip_2": "Defined as 100 * (1 - S / T_p). Lower is better.", - "menu.advanced.log_level": "Log Level", - "menu.advanced.log_level_critical": "critical", - "menu.advanced.log_level_debug": "debug", - "menu.advanced.log_level_err": "err", - "menu.advanced.log_level_info": "info", - "menu.advanced.log_level_off": "off", - "menu.advanced.log_level_tooltip": "Log level. Trace is most verbose. Default is info.", - "menu.advanced.log_level_trace": "trace", - "menu.advanced.log_level_warn": "warn", - "menu.advanced.makespan_label": "Makespan (T_p)", - "menu.advanced.makespan_metric": "Makespan (T_p): %s", - "menu.advanced.makespan_tooltip": "Observed wall-clock duration for the full shader build.", - "menu.advanced.open_logs": "Open Logs", - "menu.advanced.parallelism_header": "Parallelism (derived from %zu compiled tasks)", - "menu.advanced.parallelism_tooltip_1": "Computed lazily from the last completed build.", - "menu.advanced.parallelism_tooltip_2": "Only evaluated when this Statistics section is open.", "menu.advanced.pixel": "Pixel", "menu.advanced.pixel_tooltip": "Replace Pixel Shaders. When false, will disable the custom Pixel Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", "menu.advanced.press_key_shader_block_next": "Press any key for Shader Block Next...", "menu.advanced.press_key_shader_block_prev": "Press any key for Shader Block Previous...", - "menu.advanced.queue_wait_metric": "Queue wait (avg/max): %s / %s", - "menu.advanced.queue_wait_tooltip_1": "Time spent waiting in the ready queue before a worker started compilation.", - "menu.advanced.queue_wait_tooltip_2": "Useful for identifying scheduler-induced delay separate from compile cost.", - "menu.advanced.relative_bar_format": "{} ({:.1f}%)", - "menu.advanced.relative_durations": "Relative durations (normalized)", - "menu.advanced.replace_original_shaders": "Replace Original Shaders", "menu.advanced.shader_blocking_active": "Shader Blocking Active", "menu.advanced.shader_class_label": "Class: %s", - "menu.advanced.shader_compiler_stats": "Shader Compiler : {}", - "menu.advanced.shader_debug_header": "Shader Debug", "menu.advanced.shader_defines": "Shader Defines", "menu.advanced.shader_defines_tooltip": "Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile.", "menu.advanced.shader_descriptor": "Descriptor: 0x%X", "menu.advanced.shader_row_tooltip": "Type: {}\nClass: {}\nDescriptor: 0x{:X}\nKey: {}\n\n{}", - "menu.advanced.shader_slow_entry": "#%zu %s (weight %d)", "menu.advanced.shader_type_label": "Type: %s", - "menu.advanced.span_label": "Span (S)", - "menu.advanced.span_metric": "Span (S, longest): %s", - "menu.advanced.span_tooltip_1": "Critical-path lower bound, approximated by the single slowest shader.", - "menu.advanced.span_tooltip_2": "Even infinite cores cannot finish faster than this.", - "menu.advanced.statistics": "Statistics", "menu.advanced.stop_blocking": "Stop Blocking##Section", - "menu.advanced.tab_developer": "Developer", - "menu.advanced.tab_disable_at_boot": "Disable at Boot", - "menu.advanced.tab_logging": "Logging", - "menu.advanced.tab_shader_debug": "Shader Debug", - "menu.advanced.tab_testing": "Testing", "menu.advanced.test_conditions": "Test Conditions", - "menu.advanced.top_slowest_shaders": "Top %zu Slowest Shaders (last build)", "menu.advanced.vertex": "Vertex", "menu.advanced.vertex_tooltip": "Replace Vertex Shaders. When false, will disable the custom Vertex Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. ", - "menu.advanced.work_label": "Work (W)", - "menu.advanced.work_metric": "Work (W, sum of task wall times): %s", - "menu.advanced.work_tooltip_1": "Total compile work: sum of all per-shader wall-clock compile times.", - "menu.advanced.work_tooltip_2": "This is not CPU time; it is accumulated task elapsed time.", - "menu.advanced.work_tooltip_3": "Equivalent serial time on one worker if overhead stayed the same.", "menu.clear_shader_cache": "Clear Shader Cache", "menu.clear_shader_cache_tooltip": "Clears the shader cache and disk cache (if enabled). The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. The Disk Cache is a collection of compiled shaders on disk. Clearing will mean that shaders are recompiled only when the game re-encounters them.", "menu.disable_at_boot_desc": "Select features to disable at boot. This is the same as deleting a feature.ini file. Restart will be required to reenable.", - "menu.faq.a1": "Community Shaders is a comprehensive graphics enhancement framework for Skyrim that provides advanced lighting, materials, and visual effects. It's designed to be modular, allowing you to enable only the features you want while maintaining good performance.", "menu.faq.a2": "Each feature can be found in the left sidebar menu. Click on any feature to access its settings. Most features include presets and detailed tooltips to help you understand what each setting does.", "menu.faq.a3": "Features may fail to load due to hardware incompatibility, missing dependencies, or conflicts with other mods. Check the 'Feature Issues' tab for detailed information about any problematic features.", "menu.faq.a4": "Failed shaders are usually caused by mixed file versions. Ensure all features are up to date and avoid mixing files from test builds or outdated versions. Please review the 'Feature Issues' tab and/or Wiki for more information. Update your features and remove any obsolete features.", "menu.faq.a5": "Start by enabling the Performance Overlay to monitor your FPS. Consider disabling expensive features like Screen Space GI or reducing quality settings. The 'Display' tab also includes upscaling options that can improve performance.", - "menu.faq.a6": "No, Community Shaders is not compatible with ENB. Community Shaders will automatically disable itself if ENB is detected.", - "menu.faq.a7": "By default, Community Shaders uses the END key to open this menu. If your keyboard doesn't have an END key or it's not working, you can change it in the General > Keybindings tab. You can also edit the hotkey in the JSON configuration files.", - "menu.faq.a8": "We're always looking for talented developers to join the team! Check out our GitHub wiki for contribution guidelines and join our Discord server to connect with the development team. Whether you're interested in shader programming, C++ development, or documentation, there's always something to contribute.", - "menu.faq.a9": "Yes! Community Shaders is completely open source and available on GitHub. You can view the source code, report issues, suggest features, and contribute to the project. The project is licensed under GPL, ensuring it remains free and open for everyone. Branding materials and assets (icons, nexus branding, typography, etc) are not covered by the GPL Licence. Any included assets may not be used without explicit permission.", - "menu.faq.q1": "What is Community Shaders?", "menu.faq.q2": "How do I configure features?", "menu.faq.q3": "Why are some features not loading?", "menu.faq.q4": "I have \"Failed Shaders\" when compiling?", "menu.faq.q5": "How do I improve performance?", - "menu.faq.q6": "Is Community Shaders compatible with ENB?", - "menu.faq.q7": "The menu hotkey isn't working!", - "menu.faq.q8": "I would like to help develop Community Shaders.", - "menu.faq.q9": "Is Community Shaders open source?", "menu.faq.title": "Frequently Asked Questions", "menu.features": "Features", "menu.features.advanced": "Advanced", @@ -1763,27 +1614,16 @@ "menu.home.constraint_header_forced_to": "Forced To", "menu.home.constraint_header_setting": "Setting", "menu.home.constraints_desc": "Some settings are constrained by other features. Hover over rows for details.", - "menu.home.dev_wiki": "Developer Wiki", - "menu.home.github": "GitHub", - "menu.home.intro": "Community Shaders provides advanced graphics enhancements for Skyrim.\nThis comprehensive collection of features brings modern rendering techniques\nto enhance your visual experience.", - "menu.home.join_discord": "Join our Discord", - "menu.home.nexus_mods": "Nexus Mods", "menu.home.quick_links": "Quick Links", - "menu.home.welcome": "Welcome to Community Shaders {version}", - "menu.home.welcome_dev": "Welcome to Community Shaders {version} [{build}]", - "menu.home.wiki": "Wiki", "menu.issues.all_ini_loading": "All feature INI files are loading successfully.", "menu.issues.cancel": "Cancel", "menu.issues.cannot_be_undone": "This action cannot be undone!", - "menu.issues.check_modified_files": "Check for modified files in Data/Shaders/ (not in feature subfolders)", "menu.issues.cleanup_actions": "Cleanup Actions:", "menu.issues.clear_issue_list": "Clear Issue List", "menu.issues.clear_issue_list_tooltip": "Clears this issue list (useful after cleanup).", "menu.issues.compilation_breaking_desc": "The following features modified core shader files and must be completely uninstalled via your mod manager. Deleting just the INI file will not fix compilation errors if core shaders were modified.", "menu.issues.compilation_breaking_header": "Compilation Breaking Features", - "menu.issues.compilation_persist_warning": "If compilation issues persist after deletion:", "menu.issues.core_feature_installed": "Core feature already installed", - "menu.issues.core_feature_installed_tooltip": "This feature is already included as part of the core Community Shaders installation. Uninstall this feature with your mod manager.", "menu.issues.current_version": "Current Version: %s", "menu.issues.delete": "Delete", "menu.issues.delete_confirm": "Are you sure? This will delete all files for feature '%s'?", @@ -1816,26 +1656,16 @@ "menu.issues.override_failures_desc": "The following override files failed to load or apply. Check the file format and content.", "menu.issues.override_failures_header": "Override Failures", "menu.issues.potential_compilation_failure": "POTENTIAL COMPILATION FAILURE", - "menu.issues.reinstall_cs": "Consider reinstalling Community Shaders if issues persist", "menu.issues.replaced_by_prefix": "(replaced by ", "menu.issues.replaced_by_suffix": ")", "menu.issues.replacement_label": "Replacement: %s", "menu.issues.shader_directory_label": "Shader directory: %s", "menu.issues.shader_folder": "Shader Folder: %s", "menu.issues.test.active_inis_count": "Active test INI files ({count}):\n", - "menu.issues.test.active_inis_warning": "Test INI files are currently active. Restart CS to see feature issues.", - "menu.issues.test.create_test_inis": "Create Test Inis", - "menu.issues.test.create_test_inis_tooltip": "Creates test INI files that trigger all known feature issue cases:\n- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n- Unknown features (fake non-existent features)\n- Version mismatch (modifies existing feature version)\nRestart CS after creating to see the issues in action.", - "menu.issues.test.feature_issue_testing": "Feature Issue Testing", - "menu.issues.test.feature_issue_testing_desc": "These tools create test INI files to trigger all known feature issue types for testing purposes.", "menu.issues.test.modified_notice": "\nSome test files modified - restore recommended to clean up", "menu.issues.test.no_active_inis": "No test INI files are currently active.", - "menu.issues.test.restore": "Restore", - "menu.issues.test.restore_tooltip": "Removes all test INI files and restores any modified INI files to their original state.\nThis undoes all changes made by 'Create Test Inis'.\nRestart CS after restoring to see normal operation.", - "menu.issues.test.testing_header": "Testing", "menu.issues.this_will_delete": "This will delete:", "menu.issues.time_label": "Time: %s", - "menu.issues.uninstall_via_mod_manager": "Completely uninstall the feature via your mod manager", "menu.issues.unknown_compilation_warning": "This unknown feature may have modified core shader files and could be causing compilation failures. Unknown features should be removed if failures continue.", "menu.issues.unknown_delete_warning": "This is an UNKNOWN feature. If it modified core shader files (outside of its own folder), deleting these files alone will NOT fix shader compilation issues.", "menu.issues.unknown_features_desc": "The following features are not recognized and we tried to disable automatically. They may be from development branches or newer CS versions. Since we cannot determine what files they may have modified, they should be removed as a precaution to prevent potential shader compilation failures.", @@ -1873,7 +1703,7 @@ "menu.settings.cancel": "Cancel", "menu.settings.cell_padding": "Cell Padding", "menu.settings.center_header_title": "Center Header Title", - "menu.settings.center_header_title_tooltip": "Centers the Community Shaders title and logo in the header title bar", + "menu.settings.center_header_title_tooltip": "Centers the title and logo in the header title bar", "menu.settings.child_border_size": "Child Border Size", "menu.settings.child_rounding": "Child Rounding", "menu.settings.color_background": "Background", @@ -2047,7 +1877,7 @@ "menu.settings.show_footer_tooltip": "Shows the footer with game version, swap chain, and GPU information at the bottom of the window", "menu.settings.show_icon_buttons_in_header": "Show Icon Buttons in Header", "menu.settings.show_icon_buttons_in_header_tooltip": "When enabled: Shows action buttons (Save, Load, Clear Cache) as icons in the header\nWhen disabled: Shows as text buttons below the header", - "menu.settings.skip_clear_cache_dialogue": "Skip Clear Cache Dialogue", + "menu.settings.skip_clear_cache_dialogue": "Skip Clear Cache Confirmation", "menu.settings.skip_clear_cache_dialogue_tooltip": "When checked, the shader cache will be cleared immediately without asking for confirmation.", "menu.settings.skip_compilation_key": "Skip Compilation Key:", "menu.settings.skip_unchanged_shaders": "Skip Unchanged Shaders", @@ -2092,7 +1922,7 @@ "menu.settings.use_custom_shaders": "Use Custom Shaders", "menu.settings.use_custom_shaders_tooltip": "Disabling this effectively disables all features.", "menu.settings.use_monochrome_cs_logo": "Use Monochrome CS Logo", - "menu.settings.use_monochrome_cs_logo_tooltip": "Uses monochrome version of the Community Shaders logo", + "menu.settings.use_monochrome_cs_logo_tooltip": "Uses monochrome version of the logo", "menu.settings.use_monochrome_icons": "Use Monochrome Icons", "menu.settings.use_monochrome_icons_tooltip": "Uses white monochrome icons that adapt to your theme's text color", "menu.settings.use_resolution_based_font_size": "Use resolution-based font size", @@ -2105,14 +1935,10 @@ "menu.setup.choose_hotkey": "Please choose a hotkey to access the menu:", "menu.setup.cs_editor_unbound": "CS Editor hotkey unbound - chosen key uses Shift", "menu.setup.cs_editor_will_be": "CS Editor hotkey will be: {key}", - "menu.setup.new_install_line1": "This appears to be a new install, update, or", - "menu.setup.new_install_line2": "reinstallation of Community Shaders.", "menu.setup.press_any_key": "Press any key to set as toggle key...", "menu.setup.press_to_close": "Press Escape or Enter to continue", "menu.toggle_error_message": "Toggle Error Message", "menu.toggle_error_message_tooltip": "Hide or show the shader failure message. Your installation is broken and will likely see errors in game. Please double check you have updated all features and that your load order is correct. See CommunityShaders.log for details and check the Nexus Mods page or Discord server.", - "menu.window_title": "Community Shaders {version}", - "menu.window_title_dev": "Community Shaders {version} [{build}]", "overlay.modified_features": "Features that may have modified shaders detected. Check Feature Issues in the Menu.", "overlay.shader_blocking_active": "Shader Blocking Active", "overlay.uncompiled_warning": "WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading.", diff --git a/src/Hooks.cpp b/src/Hooks.cpp index f2d941a7cb..cbf0021d1a 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -569,9 +569,6 @@ namespace Hooks static inline REL::Relocation func; }; - // kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader - // writes accumulated wetness/sparkle values that exceed the 8-bit range and - // quantize into visible banding on tessellated snow. Promote to fp16 for headroom. // kSNOW / kSNOW_SWAP are created at R8G8B8A8_UNORM by vanilla; the snow shader // writes accumulated wetness/sparkle values that exceed the 8-bit range and // quantize into visible banding on tessellated snow. Promote to fp16 for headroom. From 0abdfd8ed22fdbd8c6c85094f3fa033884757118 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 20:43:18 -0700 Subject: [PATCH 52/55] fix(fog): init out-params to clear fxc X4000 (max-warnings 0) GetSceneDepthForFog assigns volumeUV/projectedDepth on every path, but the [branch] early-return trips fxc's uninitialized-variable analysis (X4000), which fails the fork's shader validation (--max-warnings 0). Initialize the out-params at function entry and the two caller locals. Behavior-preserving (values were already assigned before use). Same class as upstream #2478/#2479. --- .../ExponentialHeightFog/ExponentialHeightFog.hlsli | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index c15a0280d3..2a5ee602e3 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -46,6 +46,11 @@ namespace ExponentialHeightFog float GetSceneDepthForFog(float3 positionWS, uint eyeIndex, out float2 volumeUV, out float projectedDepth) { + // Init out-params at entry: fxc's flow analysis flags X4000 on the [branch] + // early-return path otherwise (every path already assigns these). + volumeUV = 0.0f.xx; + projectedDepth = 0.0f; + float4 clipPosition = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1.0f)); [branch] if (clipPosition.w <= 0.0f) { @@ -73,8 +78,8 @@ namespace ExponentialHeightFog if (volumeWidth == 0 || volumeHeight == 0 || volumeDepth == 0) return float4(0.0f, 0.0f, 0.0f, 1.0f); - float2 volumeUV; - float projectedDepth; + float2 volumeUV = 0.0f.xx; + float projectedDepth = 0.0f; float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); if (projectedDepth <= 0.0f) return float4(0.0f, 0.0f, 0.0f, 1.0f); @@ -205,8 +210,8 @@ namespace ExponentialHeightFog } uint eyeIndex = GetEyeIndexFromCameraWS(cameraWS); float3 viewToPos = positionWS; - float2 volumeUV; - float projectedDepth; + float2 volumeUV = 0.0f.xx; + float projectedDepth = 0.0f; float sceneDepth = GetSceneDepthForFog(positionWS, eyeIndex, volumeUV, projectedDepth); [branch] if (projectedDepth > 1e-4f && sceneDepth > projectedDepth) { From eb5bb8059ea749c871852955c8e55a323e034bf0 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 21:00:27 -0700 Subject: [PATCH 53/55] fix(shaders): clear remaining fxc X4000/X3206 under max-warnings 0 Local full validation surfaced warnings the truncated CI log hid: - ExponentialHeightFog GetSceneDepthForFog: entry-init alone didn't satisfy fxc; the early return inside [branch] still trips X4000. Restructure to single-exit (out-params assigned once; behind-camera keeps the zero init). - ISTemporalAA EncodeFeedbackLuma/DecodeFeedbackLuma: X3206 implicit truncation -- Color::LinearToGammaSafe/GammaToLinearSafe are float3->float3; apply the .xxx-in/.x-out scalar pattern already used on the PQ helpers. Verified locally: hlslkit Flatrim config -> 0 new warnings, 0 errors. --- .../ExponentialHeightFog.hlsli | 16 ++++++---------- package/Shaders/ISTemporalAA.hlsl | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index 2a5ee602e3..c76d7295c7 100644 --- a/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli +++ b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli @@ -46,23 +46,19 @@ namespace ExponentialHeightFog float GetSceneDepthForFog(float3 positionWS, uint eyeIndex, out float2 volumeUV, out float projectedDepth) { - // Init out-params at entry: fxc's flow analysis flags X4000 on the [branch] - // early-return path otherwise (every path already assigns these). + // Single-exit, out-params assigned once: an early return inside the [branch] + // trips fxc's uninitialized-variable analysis (X4000) even though every path + // assigns these. Behind-camera (w<=0) leaves the zero-initialized values. volumeUV = 0.0f.xx; projectedDepth = 0.0f; float4 clipPosition = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1.0f)); - [branch] if (clipPosition.w <= 0.0f) + [branch] if (clipPosition.w > 0.0f) { - volumeUV = 0.0f.xx; - projectedDepth = 0.0f; - return 0.0f; + projectedDepth = GetSceneDepthFromClip(clipPosition); + volumeUV = saturate(clipPosition.xy / clipPosition.w * float2(0.5f, -0.5f) + 0.5f); } - projectedDepth = GetSceneDepthFromClip(clipPosition); - volumeUV = clipPosition.xy / clipPosition.w * float2(0.5f, -0.5f) + 0.5f; - - volumeUV = saturate(volumeUV); return projectedDepth; } diff --git a/package/Shaders/ISTemporalAA.hlsl b/package/Shaders/ISTemporalAA.hlsl index 4b71469bf0..6b3214fe4e 100644 --- a/package/Shaders/ISTemporalAA.hlsl +++ b/package/Shaders/ISTemporalAA.hlsl @@ -63,11 +63,11 @@ float EncodeFeedbackLuma(float pqLuma) { // PQ → linear (single channel: luma only, no colour transform needed) float linearLuma = DisplayMapping::PQtoLinear(pqLuma.xxx, 10000.0).x; - return Color::LinearToGammaSafe(linearLuma); + return Color::LinearToGammaSafe(linearLuma.xxx).x; } float DecodeFeedbackLuma(float gammaLuma) { - float linearLuma = Color::GammaToLinearSafe(gammaLuma); + float linearLuma = Color::GammaToLinearSafe(gammaLuma.xxx).x; return DisplayMapping::LinearToPQ(linearLuma.xxx, 10000.0).x; } # endif From 4698d0fb59392a0280d68d545afff07d856a8499 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 21:42:21 -0700 Subject: [PATCH 54/55] chore(i18n): re-sort zh_CN.json to match en.json key order After regenerating en.json (dropped orphaned fork keys), zh_CN.json key order no longer matched, failing the sort-i18n.py --check step. Re-sorted via tools/sort-i18n.py --write (pure reorder, no key changes). --- .../CommunityShaders/Translations/zh_CN.json | 350 +++++++++--------- 1 file changed, 175 insertions(+), 175 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 30264ab701..63e09b935e 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -545,14 +545,12 @@ "feature.cs_editor.wind_vs_player_tooltip_1": "- ~0° = 顺风(风在玩家背后)", "feature.cs_editor.wind_vs_player_tooltip_2": "- ~±90° = 横风(左/右)", "feature.cs_editor.wind_vs_player_tooltip_3": "- ~±180° = 逆风(风朝向玩家)", - "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", "feature.dynamic_cubemaps.color": "颜色", "feature.dynamic_cubemaps.creator_info": "您必须通过添加着色器定义 CREATOR 来启用创作者模式", "feature.dynamic_cubemaps.description": "通过生成捕获周围环境的动态立方体贴图,提供实时环境映射和反射,实现逼真的表面反射效果。", "feature.dynamic_cubemaps.dynamic_cubemap_creator": "动态立方体贴图创建器", "feature.dynamic_cubemaps.enable_creator": "启用创建器", "feature.dynamic_cubemaps.enable_ssr": "启用屏幕空间反射", - "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", "feature.dynamic_cubemaps.export": "导出", "feature.dynamic_cubemaps.key_feature_1": "实时环境捕获,生成逼真反射", "feature.dynamic_cubemaps.key_feature_2": "基于摄像机位置的动态立方体贴图生成", @@ -562,7 +560,6 @@ "feature.dynamic_cubemaps.name": "动态立方体贴图", "feature.dynamic_cubemaps.roughness": "粗糙度", "feature.dynamic_cubemaps.screen_space_reflections": "屏幕空间反射", - "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", "feature.exp_height_fog.apply_vanilla_fade": "应用原版渐隐", "feature.exp_height_fog.apply_vanilla_fade_tooltip": "将原版渐隐亮度应用于指数高度雾。", "feature.exp_height_fog.cubemap_mip_level": "立方体贴图Mip级别", @@ -849,21 +846,7 @@ "feature.light_editor.sort_by": "排序方式", "feature.light_editor.spotlight_not_applicable": "聚光灯:ISL光源类型标志不适用", "feature.light_editor.total_lights": "总光源数:%u", - "feature.light_limit_fix.debug": "调试", - "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", - "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", - "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", - "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", - "feature.light_limit_fix.key_feature_1": "移除4光源限制", - "feature.light_limit_fix.key_feature_2": "无限动态光源", - "feature.light_limit_fix.key_feature_3": "提升光照质量", - "feature.light_limit_fix.key_feature_4": "增强视觉真实感", - "feature.light_limit_fix.key_feature_5": "增强视觉真实感", - "feature.light_limit_fix.light_limit_vis": "光源限制可视化", - "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", - "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", "feature.light_limit_fix.name": "光源限制修复", - "feature.light_limit_fix.statistics": "统计", "feature.linear_lighting.ambient_gamma": "环境伽马", "feature.linear_lighting.ambient_multiplier": "环境倍率", "feature.linear_lighting.blood_effects_multiplier": "血液效果倍率", @@ -983,17 +966,12 @@ "feature.renderdoc.disk_usage": "磁盘使用量", "feature.renderdoc.disk_usage_tooltip": "监控捕获存储使用情况", "feature.renderdoc.double_click_hint": "双击文件名以打开捕获文件", - "feature.renderdoc.enable_capture": "启用RenderDoc捕获", - "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", - "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", "feature.renderdoc.hover_hint": "悬停在文件名上查看文件详情", "feature.renderdoc.no_files": "未找到捕获文件。", "feature.renderdoc.not_enough_space": "没有足够的可用磁盘空间来创建捕获。", "feature.renderdoc.ok": "确定", "feature.renderdoc.open_capture_dir": "打开捕获目录", "feature.renderdoc.refresh_list": "刷新列表", - "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", - "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", "feature.renderdoc.space_required": "至少需要{} MB的可用空间。", "feature.renderdoc.yes_delete": "是,全部删除", "feature.screen_space_gi.ao_only": "仅AO", @@ -1007,7 +985,6 @@ "feature.screen_space_gi.denoising": "降噪", "feature.screen_space_gi.depth_fade_range": "深度渐隐范围", "feature.screen_space_gi.depth_fade_range_tooltip": "基于深度的效果渐隐的距离范围。", - "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", "feature.screen_space_gi.enabled": "启用", "feature.screen_space_gi.enabled_tooltip": "启用屏幕空间全局光照。禁用时,所有其他设置将被忽略。", "feature.screen_space_gi.extreme": "极致", @@ -1025,11 +1002,6 @@ "feature.screen_space_gi.il_saturation": "IL饱和度", "feature.screen_space_gi.il_source_brightness": "IL源亮度", "feature.screen_space_gi.indirect_lighting": "间接光照(IL)", - "feature.screen_space_gi.key_feature_1": "逼真的间接光照", - "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", - "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", - "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", - "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", "feature.screen_space_gi.low": "低", "feature.screen_space_gi.low_tooltip": "四分之一分辨率且模糊。", "feature.screen_space_gi.max_frame_accumulation": "最大帧累积", @@ -1061,7 +1033,6 @@ "feature.screen_space_gi.view_resize": "视图调整大小", "feature.screen_space_gi.visual": "视觉", "feature.screen_space_gi.visual_il": "视觉 - IL", - "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", "feature.screen_space_shadows.bilinear_threshold": "双线性阈值", "feature.screen_space_shadows.bilinear_threshold_tooltip": "双线性插值期间边缘检测的深度阈值。较高值跨边缘更积极地平滑。", "feature.screen_space_shadows.description": "通过添加详细的接触阴影和提高阴影精度来增强阴影质量。\n此技术添加了传统阴影映射可能遗漏的精细细节阴影。", @@ -1322,7 +1293,6 @@ "feature.unified_water.regenerate_flowmap": "重新生成流图", "feature.unified_water.use_optimised_meshes": "使用优化网格", "feature.unified_water.use_optimised_meshes_tooltip": "使用三角面数显著更低的网格以提升性能,视觉质量无损。\n仅影响新创建的水体 - 需要切换位置或重启游戏才能生效。", - "feature.upscaling.backend_diagnostics": "后端诊断", "feature.upscaling.description": "先进的超分辨率和帧生成技术,提升游戏性能。", "feature.upscaling.dlss_model_preset": "DLSS模型预设", "feature.upscaling.dlss_model_preset_default": "默认", @@ -1330,21 +1300,9 @@ "feature.upscaling.dlss_model_preset_k": "预设 K", "feature.upscaling.dlss_model_preset_l": "预设 L", "feature.upscaling.dlss_model_preset_m": "预设 M", - "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", - "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", "feature.upscaling.fps_limit": "FPS限制", "feature.upscaling.fps_limit_tooltip_1": "设置帧率上限目标。", "feature.upscaling.fps_limit_tooltip_2": "起始值设置为比刷新率低2-3 FPS(例如120 Hz为117)。", - "feature.upscaling.frame_generation": "帧生成", - "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", - "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", - "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", - "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", - "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", - "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", - "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", - "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", - "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", "feature.upscaling.key_feature_1": "DLSS(深度学习超采样)支持", "feature.upscaling.key_feature_2": "FSR(FidelityFX超分辨率)支持", "feature.upscaling.key_feature_3": "TAA(时间抗锯齿)支持", @@ -1356,7 +1314,6 @@ "feature.upscaling.low_latency_mode_tooltip_1": "通过将CPU工作更紧密地与GPU同步来减少输入延迟。", "feature.upscaling.low_latency_mode_tooltip_2": "可能略微降低最大FPS,但通常感觉响应更快。", "feature.upscaling.marker_optimization_unavailable": "标记优化不可用(PCL未加载)。", - "feature.upscaling.method": "方法", "feature.upscaling.method_none": "无", "feature.upscaling.method_taa": "TAA", "feature.upscaling.name": "超分辨率", @@ -1372,9 +1329,6 @@ "feature.upscaling.reflex_not_available": "Reflex不可用。请确保sl.reflex.dll存在并重启。", "feature.upscaling.sharpness": "锐度", "feature.upscaling.streamline_logging": "Streamline日志记录", - "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", - "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", - "feature.upscaling.upscale_preset": "升频预设", "feature.upscaling.upscaling_intermediates": "升频中间结果", "feature.upscaling.use_fps_limit": "使用FPS限制", "feature.upscaling.use_fps_limit_tooltip_1": "使用Reflex内部FPS上限以获得更稳定的帧时间。", @@ -1385,7 +1339,6 @@ "feature.upscaling.view_resize": "视图调整大小", "feature.upscaling.vr_intermediates_not_created": "VR中间结果尚未创建(进入游戏世界)", "feature.volumetric_lighting.description": "体积光照通过雾、尘埃和大气粒子创造逼真的光散射效果。\n为室内和室外环境添加戏剧化的上帝射线和大气深度。", - "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", "feature.volumetric_lighting.enable_interiors": "在室内启用体积光照", "feature.volumetric_lighting.exterior_depth": "室外深度", "feature.volumetric_lighting.exterior_height": "室外高度", @@ -1411,31 +1364,6 @@ "feature.volumetric_shadows.key_feature_3": "多级联支持", "feature.volumetric_shadows.key_feature_4": "针对效果渲染优化", "feature.volumetric_shadows.name": "体积阴影", - "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", - "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", - "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", - "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", - "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", - "feature.vr.key_feature_5": "可配置的遮挡剔除参数", - "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "调试", - "feature.vr_stereo.debug_pom_depth": "调试POM深度", - "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", - "feature.vr_stereo.enable": "启用", - "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", - "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", - "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", - "feature.vr_stereo.full_blend_distance": "完全混合距离", - "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", - "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", - "feature.vr_stereo.off": "关闭", - "feature.vr_stereo.pom_depth_scale": "POM深度缩放", - "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", - "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", - "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", "feature.water_effects.description": "通过逼真的焦散和水下光照效果增强水面渲染。\n添加动态光影图案并提升水面视觉质量。", "feature.water_effects.key_feature_1": "逼真的水面焦散", "feature.water_effects.key_feature_2": "增强的水下光照", @@ -1579,16 +1507,7 @@ "feature.wetness_effects.weather_transition_speed_tooltip": "下雨时湿润效果出现的速度以及雨停后干燥的速度。", "feature.wetness_effects.wetness_effects": "湿润效果", "feature.wetness_effects.wetness_in_exterior": "湿润度 室内/室外", - "menu.advanced.active_shaders": "活跃着色器", "menu.advanced.active_shaders_tooltip": "最近帧中使用过的着色器列表。在上方启用着色器拦截可使用热键循环浏览并拦截着色器进行调试。约1秒未使用的着色器将从此列表中移除。", - "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", - "menu.advanced.addresses": "地址", - "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", - "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", - "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", - "menu.advanced.avoid_flow_control": "避免流控制", - "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.background_compiler_threads": "后台编译器线程", "menu.advanced.background_compiler_threads_tooltip": "游戏过程中用于编译着色器的线程数。默认为性能核心的一半,以避免影响渲染线程。较高值可更快完成编译,但可能导致卡顿。", "menu.advanced.block_next": "拦截下一个:", "menu.advanced.block_previous": "拦截上一个:", @@ -1609,109 +1528,41 @@ "menu.advanced.column_key_tooltip": "着色器键", "menu.advanced.column_type": "类型", "menu.advanced.column_type_tooltip": "着色器类型", - "menu.advanced.compiler_threads": "编译器线程", "menu.advanced.compiler_threads_tooltip": "启动时用于编译着色器的线程数。默认为所有逻辑核心减去一个以留出系统开销(包含E核)。较高值可更快完成编译,但可能降低系统响应性。", "menu.advanced.compute": "计算", "menu.advanced.compute_tooltip": "替换计算着色器。设为false时将禁用上述类型的自定义计算着色器。供开发者测试CS着色器是否与原版行为匹配。", "menu.advanced.copy_info": "复制信息", "menu.advanced.copy_info_tooltip": "将包含缓存路径的完整着色器信息复制到剪贴板", - "menu.advanced.copy_key": "复制键", - "menu.advanced.dump_ini_settings": "导出INI设置", "menu.advanced.dump_shaders": "导出着色器", "menu.advanced.dump_shaders_tooltip": "在启动时导出着色器。仅在逆向着色器时使用。普通用户无需此功能。", - "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", - "menu.advanced.enable_file_watcher": "启用文件监视器", - "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", "menu.advanced.enable_shader_blocking": "启用着色器拦截", "menu.advanced.enable_shader_blocking_tooltip": "启用热键以循环浏览并拦截单个着色器,用于调试目的。", - "menu.advanced.frame_annotations": "帧注释", - "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", - "menu.advanced.half_precision": "半精度(部分精度)", - "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.infinite_core_efficiency": "无限核心效率", - "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", - "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", - "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", - "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", - "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", - "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", - "menu.advanced.log_level": "日志级别", - "menu.advanced.log_level_critical": "严重", - "menu.advanced.log_level_debug": "调试", - "menu.advanced.log_level_err": "错误", - "menu.advanced.log_level_info": "信息", - "menu.advanced.log_level_off": "关闭", - "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", - "menu.advanced.log_level_trace": "跟踪", - "menu.advanced.log_level_warn": "警告", - "menu.advanced.makespan_label": "完工时间(T_p)", - "menu.advanced.makespan_metric": "完工时间(T_p):%s", - "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", - "menu.advanced.open_logs": "打开日志", - "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", - "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", - "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", "menu.advanced.pixel": "像素", "menu.advanced.pixel_tooltip": "替换像素着色器。设为false时将禁用上述类型的自定义像素着色器。供开发者测试CS着色器是否与原版行为匹配。", "menu.advanced.press_key_shader_block_next": "按下任意键设置着色器拦截下一个...", "menu.advanced.press_key_shader_block_prev": "按下任意键设置着色器拦截上一个...", - "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", - "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", - "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", - "menu.advanced.relative_bar_format": "{}({:.1f}%)", - "menu.advanced.relative_durations": "相对持续时间(归一化)", - "menu.advanced.replace_original_shaders": "替换原始着色器", "menu.advanced.shader_blocking_active": "着色器拦截已激活", "menu.advanced.shader_class_label": "类别:%s", - "menu.advanced.shader_compiler_stats": "着色器编译器:{}", - "menu.advanced.shader_debug_header": "着色器调试", "menu.advanced.shader_defines": "着色器定义", "menu.advanced.shader_defines_tooltip": "着色器编译器的定义。以分号\";\"分隔。用空格清除。更改后需重建着色器。计算着色器需要重启才能重新编译。", "menu.advanced.shader_descriptor": "描述符:0x%X", "menu.advanced.shader_row_tooltip": "类型:{}\n类别:{}\n描述符:0x{:X}\n键:{}\n\n{}", - "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", "menu.advanced.shader_type_label": "类型:%s", - "menu.advanced.span_label": "跨度(S)", - "menu.advanced.span_metric": "跨度(S,最长):%s", - "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", - "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", - "menu.advanced.statistics": "统计", "menu.advanced.stop_blocking": "停止拦截##Section", - "menu.advanced.tab_developer": "开发者", - "menu.advanced.tab_disable_at_boot": "启动时禁用", - "menu.advanced.tab_logging": "日志记录", - "menu.advanced.tab_shader_debug": "着色器调试", - "menu.advanced.tab_testing": "测试", "menu.advanced.test_conditions": "测试条件", - "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", "menu.advanced.vertex": "顶点", "menu.advanced.vertex_tooltip": "替换顶点着色器。设为false时将禁用上述类型的自定义顶点着色器。供开发者测试CS着色器是否与原版行为匹配。", - "menu.advanced.work_label": "工作量(W)", - "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", - "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", - "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", - "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", "menu.clear_shader_cache": "清除着色器缓存", "menu.clear_shader_cache_tooltip": "清除着色器缓存和磁盘缓存(如果启用)。\n着色器缓存是在运行时替换原版着色器的已编译着色器集合。\n磁盘缓存是磁盘上已编译着色器的集合。清除后意味着着色器仅在游戏再次遇到它们时才重新编译。", "menu.disable_at_boot_desc": "选择要在启动时禁用的功能。这与删除feature.ini文件相同。重新启用需要重启。", - "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", "menu.faq.a2": "每个功能都可以在左侧边栏菜单中找到。点击任何功能即可访问其设置。大多数功能包含预设和详细的工具提示,帮助您了解每个设置的作用。", "menu.faq.a3": "功能可能因硬件不兼容、依赖项缺失或与其他模组冲突而无法加载。请查看\"功能问题\"选项卡,了解有关任何有问题的功能的详细信息。", "menu.faq.a4": "着色器失败通常由混合文件版本引起。请确保所有功能均为最新,并避免混合测试版本或过时版本的文件。请查看\"功能问题\"选项卡和/或Wiki了解更多信息。更新您的功能并移除任何过时的功能。", "menu.faq.a5": "首先启用性能叠加层来监控您的FPS。考虑禁用屏幕空间GI等占用资源的功能或降低质量设置。\"显示\"选项卡还包含可以提升性能的升频选项。", - "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", - "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", - "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", - "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", - "menu.faq.q1": "什么是Community Shaders?", "menu.faq.q2": "如何配置功能?", "menu.faq.q3": "为什么有些功能无法加载?", "menu.faq.q4": "编译时出现“着色器失败”?", "menu.faq.q5": "如何提升性能?", - "menu.faq.q6": "Community Shaders与ENB兼容吗?", - "menu.faq.q7": "菜单热键无效!", - "menu.faq.q8": "我想帮助开发Community Shaders。", - "menu.faq.q9": "Community Shaders是开源的吗?", "menu.faq.title": "常见问题解答", "menu.features": "功能", "menu.features.advanced": "高级", @@ -1761,27 +1612,16 @@ "menu.home.constraint_header_forced_to": "强制为", "menu.home.constraint_header_setting": "设置", "menu.home.constraints_desc": "某些设置受其他功能约束。悬停在行上查看详情。", - "menu.home.dev_wiki": "开发者Wiki", - "menu.home.github": "GitHub", - "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", - "menu.home.join_discord": "加入我们的Discord", - "menu.home.nexus_mods": "Nexus Mods", "menu.home.quick_links": "快速链接", - "menu.home.welcome": "欢迎使用Community Shaders {version}", - "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", - "menu.home.wiki": "Wiki", "menu.issues.all_ini_loading": "所有功能INI文件加载成功。", "menu.issues.cancel": "取消", "menu.issues.cannot_be_undone": "此操作无法撤销!", - "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", "menu.issues.cleanup_actions": "清理操作:", "menu.issues.clear_issue_list": "清除问题列表", "menu.issues.clear_issue_list_tooltip": "清除此问题列表(清理后有用)。", "menu.issues.compilation_breaking_desc": "以下功能修改了核心着色器文件,必须通过模组管理器完全卸载。如果核心着色器被修改,仅删除INI文件不会修复编译错误。", "menu.issues.compilation_breaking_header": "破坏编译的功能", - "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", "menu.issues.core_feature_installed": "核心功能已安装", - "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", "menu.issues.current_version": "当前版本:%s", "menu.issues.delete": "删除", "menu.issues.delete_confirm": "确定要删除功能'%s'的所有文件吗?", @@ -1814,26 +1654,16 @@ "menu.issues.override_failures_desc": "以下覆盖文件加载或应用失败。请检查文件格式和内容。", "menu.issues.override_failures_header": "覆盖失败", "menu.issues.potential_compilation_failure": "潜在的编译失败", - "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", "menu.issues.replaced_by_prefix": "(被替换为", "menu.issues.replaced_by_suffix": ")", "menu.issues.replacement_label": "替代:%s", "menu.issues.shader_directory_label": "着色器目录:%s", "menu.issues.shader_folder": "着色器文件夹:%s", "menu.issues.test.active_inis_count": "活动的测试 INI 文件({count}):\n", - "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", - "menu.issues.test.create_test_inis": "创建测试INI", - "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", - "menu.issues.test.feature_issue_testing": "功能问题测试", - "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", "menu.issues.test.modified_notice": "\n部分测试文件已修改 - 建议恢复以清理", "menu.issues.test.no_active_inis": "当前没有活跃的测试INI文件。", - "menu.issues.test.restore": "恢复", - "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", - "menu.issues.test.testing_header": "测试", "menu.issues.this_will_delete": "这将删除:", "menu.issues.time_label": "时间:%s", - "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", "menu.issues.unknown_compilation_warning": "此未知功能可能修改了核心着色器文件,并可能导致编译失败。如果故障继续,应移除未知功能。", "menu.issues.unknown_delete_warning": "这是一个未知功能。如果它修改了核心着色器文件(在其自身文件夹之外),仅删除这些文件不会修复着色器编译问题。", "menu.issues.unknown_features_desc": "以下功能未被识别,我们已尝试自动禁用。它们可能来自开发分支或较新的CS版本。由于我们无法确定它们可能修改了哪些文件,应作为预防措施将其移除,以防止潜在的着色器编译失败。", @@ -2090,14 +1920,10 @@ "menu.setup.choose_hotkey": "请选择一个热键来访问菜单:", "menu.setup.cs_editor_unbound": "CS 编辑器热键未绑定 - 所选键使用 Shift", "menu.setup.cs_editor_will_be": "CS 编辑器热键将为:{key}", - "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", - "menu.setup.new_install_line2": "重新安装Community Shaders。", "menu.setup.press_any_key": "按下任意键设置为切换键...", "menu.setup.press_to_close": "按Escape或Enter继续", "menu.toggle_error_message": "切换错误消息", "menu.toggle_error_message_tooltip": "隐藏或显示着色器失败消息。您的安装已损坏,游戏中可能会看到错误。请仔细检查是否已更新所有功能以及加载顺序是否正确。请参阅CommunityShaders.log了解详情,并查看Nexus Mods页面或Discord服务器。", - "menu.window_title": "Community Shaders {version}", - "menu.window_title_dev": "Community Shaders {version} [{build}]", "overlay.modified_features": "检测到可能修改了着色器的功能。请检查菜单中的功能问题。", "overlay.shader_blocking_active": "着色器拦截已激活", "overlay.uncompiled_warning": "警告:未编译的着色器在加载时会有视觉错误或导致卡顿。", @@ -2109,5 +1935,179 @@ "ui.copy": "复制", "ui.dont_ask_again": "不再提示", "ui.search": "搜索...", - "ui.search_features": "搜索功能..." + "ui.search_features": "搜索功能...", + "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", + "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", + "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", + "feature.light_limit_fix.debug": "调试", + "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", + "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", + "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", + "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", + "feature.light_limit_fix.key_feature_1": "移除4光源限制", + "feature.light_limit_fix.key_feature_2": "无限动态光源", + "feature.light_limit_fix.key_feature_3": "提升光照质量", + "feature.light_limit_fix.key_feature_4": "增强视觉真实感", + "feature.light_limit_fix.key_feature_5": "增强视觉真实感", + "feature.light_limit_fix.light_limit_vis": "光源限制可视化", + "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", + "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", + "feature.light_limit_fix.statistics": "统计", + "feature.renderdoc.enable_capture": "启用RenderDoc捕获", + "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", + "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", + "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", + "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", + "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", + "feature.screen_space_gi.key_feature_1": "逼真的间接光照", + "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", + "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", + "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", + "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", + "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", + "feature.upscaling.backend_diagnostics": "后端诊断", + "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", + "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", + "feature.upscaling.frame_generation": "帧生成", + "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", + "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", + "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", + "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", + "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", + "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", + "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", + "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", + "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", + "feature.upscaling.method": "方法", + "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", + "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", + "feature.upscaling.upscale_preset": "升频预设", + "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", + "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", + "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", + "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", + "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", + "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", + "feature.vr.key_feature_5": "可配置的遮挡剔除参数", + "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", + "feature.vr.name": "VR", + "feature.vr_stereo.debug": "调试", + "feature.vr_stereo.debug_pom_depth": "调试POM深度", + "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", + "feature.vr_stereo.enable": "启用", + "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", + "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", + "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", + "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", + "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", + "feature.vr_stereo.full_blend_distance": "完全混合距离", + "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", + "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", + "feature.vr_stereo.off": "关闭", + "feature.vr_stereo.pom_depth_scale": "POM深度缩放", + "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", + "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", + "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", + "menu.advanced.active_shaders": "活跃着色器", + "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", + "menu.advanced.addresses": "地址", + "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", + "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", + "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", + "menu.advanced.avoid_flow_control": "避免流控制", + "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.background_compiler_threads": "后台编译器线程", + "menu.advanced.compiler_threads": "编译器线程", + "menu.advanced.copy_key": "复制键", + "menu.advanced.dump_ini_settings": "导出INI设置", + "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", + "menu.advanced.enable_file_watcher": "启用文件监视器", + "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", + "menu.advanced.frame_annotations": "帧注释", + "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", + "menu.advanced.half_precision": "半精度(部分精度)", + "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", + "menu.advanced.infinite_core_efficiency": "无限核心效率", + "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", + "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", + "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", + "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", + "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", + "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", + "menu.advanced.log_level": "日志级别", + "menu.advanced.log_level_critical": "严重", + "menu.advanced.log_level_debug": "调试", + "menu.advanced.log_level_err": "错误", + "menu.advanced.log_level_info": "信息", + "menu.advanced.log_level_off": "关闭", + "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", + "menu.advanced.log_level_trace": "跟踪", + "menu.advanced.log_level_warn": "警告", + "menu.advanced.makespan_label": "完工时间(T_p)", + "menu.advanced.makespan_metric": "完工时间(T_p):%s", + "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", + "menu.advanced.open_logs": "打开日志", + "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", + "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", + "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", + "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", + "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", + "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", + "menu.advanced.relative_bar_format": "{}({:.1f}%)", + "menu.advanced.relative_durations": "相对持续时间(归一化)", + "menu.advanced.replace_original_shaders": "替换原始着色器", + "menu.advanced.shader_compiler_stats": "着色器编译器:{}", + "menu.advanced.shader_debug_header": "着色器调试", + "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", + "menu.advanced.span_label": "跨度(S)", + "menu.advanced.span_metric": "跨度(S,最长):%s", + "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", + "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", + "menu.advanced.statistics": "统计", + "menu.advanced.tab_developer": "开发者", + "menu.advanced.tab_disable_at_boot": "启动时禁用", + "menu.advanced.tab_logging": "日志记录", + "menu.advanced.tab_shader_debug": "着色器调试", + "menu.advanced.tab_testing": "测试", + "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", + "menu.advanced.work_label": "工作量(W)", + "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", + "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", + "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", + "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", + "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", + "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", + "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", + "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", + "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", + "menu.faq.q1": "什么是Community Shaders?", + "menu.faq.q6": "Community Shaders与ENB兼容吗?", + "menu.faq.q7": "菜单热键无效!", + "menu.faq.q8": "我想帮助开发Community Shaders。", + "menu.faq.q9": "Community Shaders是开源的吗?", + "menu.home.dev_wiki": "开发者Wiki", + "menu.home.github": "GitHub", + "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", + "menu.home.join_discord": "加入我们的Discord", + "menu.home.nexus_mods": "Nexus Mods", + "menu.home.welcome": "欢迎使用Community Shaders {version}", + "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", + "menu.home.wiki": "Wiki", + "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", + "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", + "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", + "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", + "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", + "menu.issues.test.create_test_inis": "创建测试INI", + "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", + "menu.issues.test.feature_issue_testing": "功能问题测试", + "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", + "menu.issues.test.restore": "恢复", + "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", + "menu.issues.test.testing_header": "测试", + "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", + "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", + "menu.setup.new_install_line2": "重新安装Community Shaders。", + "menu.window_title": "Community Shaders {version}", + "menu.window_title_dev": "Community Shaders {version} [{build}]" } From 1318a620c45189631e09dafbc7f5f6a46af7d78e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 7 Jun 2026 21:47:40 -0700 Subject: [PATCH 55/55] chore(i18n): prune zh_CN orphaned keys not in en.json The translation format check flags keys present in zh_CN.json but not en.json. These 174 keys are upstream's translations for DrawSettings strings the fork resolved to its own (unwrapped) VR-superset versions, so they are absent from source and were dropped from en.json. They render in English via the T() fallback regardless, so the zh_CN entries were dead. Pruned to satisfy zh_CN subset of en.json; tracked for re-add in the i18n wrapping follow-up. --- .../CommunityShaders/Translations/zh_CN.json | 176 +----------------- 1 file changed, 1 insertion(+), 175 deletions(-) diff --git a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json index 63e09b935e..70e24bdd9c 100644 --- a/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -1935,179 +1935,5 @@ "ui.copy": "复制", "ui.dont_ask_again": "不再提示", "ui.search": "搜索...", - "ui.search_features": "搜索功能...", - "feature.dynamic_cubemaps.advanced_vr_settings": "高级VR设置", - "feature.dynamic_cubemaps.enable_ssr_tooltip": "在水面上启用屏幕空间反射", - "feature.dynamic_cubemaps.vr_restart_required": "在VR中启用需要重启。启用后保存设置并重启游戏。", - "feature.light_limit_fix.debug": "调试", - "feature.light_limit_fix.debug_feature_enabled": "调试功能 - 光源限制可视化已启用", - "feature.light_limit_fix.description": "移除原版游戏的4光源限制,允许场景中无限数量的动态光源。\n大幅提升光照质量,实现更逼真的照明场景。", - "feature.light_limit_fix.enable_lights_vis": "启用光源可视化", - "feature.light_limit_fix.enable_lights_vis_tooltip": "启用光源限制的可视化\n", - "feature.light_limit_fix.key_feature_1": "移除4光源限制", - "feature.light_limit_fix.key_feature_2": "无限动态光源", - "feature.light_limit_fix.key_feature_3": "提升光照质量", - "feature.light_limit_fix.key_feature_4": "增强视觉真实感", - "feature.light_limit_fix.key_feature_5": "增强视觉真实感", - "feature.light_limit_fix.light_limit_vis": "光源限制可视化", - "feature.light_limit_fix.lights_vis_mode": "光源可视化模式", - "feature.light_limit_fix.lights_vis_mode_tooltip": " - 可视化光源限制。当达到\"严格\"光源限制时为红色(传送门严格光源)。\n - 可视化严格光源的数量。\n - 可视化聚类光源的数量。\n - 可视化阴影遮罩。\n", - "feature.light_limit_fix.statistics": "统计", - "feature.renderdoc.enable_capture": "启用RenderDoc捕获", - "feature.renderdoc.enable_capture_tooltip": "启用RenderDoc帧捕获,以便为Community Shaders团队提供调试捕获。", - "feature.renderdoc.enable_capture_tooltip2": "启用捕获将强制启用帧注释以便于调试,并在禁用时恢复之前的设置。", - "feature.renderdoc.restart_to_disable": "需要重启才能禁用RenderDoc捕获,性能将受到严重影响。", - "feature.renderdoc.restart_to_enable": "需要重启才能启用RenderDoc捕获。", - "feature.screen_space_gi.description": "屏幕空间全局光照为游戏添加逼真的间接光照和环境光遮蔽。此技术模拟光线在表面上反射以自然地照亮其他物体。", - "feature.screen_space_gi.key_feature_1": "逼真的间接光照", - "feature.screen_space_gi.key_feature_2": "增强的环境光遮蔽", - "feature.screen_space_gi.key_feature_3": "提升视觉深度和氛围", - "feature.screen_space_gi.key_feature_4": "时间降噪,带来平滑结果", - "feature.screen_space_gi.key_feature_5": "可配置的质量和性能设置", - "feature.screen_space_gi.vr_warning": "\n\n警告:在VR中,由于屏幕空间效果的特性,此功能可能存在视觉瑕疵并显著影响性能。", - "feature.upscaling.backend_diagnostics": "后端诊断", - "feature.upscaling.dlss_model_preset_tooltip": "选择要使用的DLSS AI模型预设。\n每个模型提供不同的视觉质量、性能和运动稳定性。\n设为\"默认\"以根据您的升频预设和硬件自动选择。\n更改此设置需要重启才能生效。", - "feature.upscaling.force_enable_frame_generation": "强制启用帧生成", - "feature.upscaling.frame_generation": "帧生成", - "feature.upscaling.frame_generation_available": "AMD FSR帧生成可用。", - "feature.upscaling.frame_generation_desc": "帧生成通过插值真实帧与生成的帧来提供更流畅的体验", - "feature.upscaling.frame_generation_in_menus": "菜单中启用帧生成", - "feature.upscaling.frame_generation_in_menus_tooltip_1": "在游戏菜单打开时保持帧生成激活。", - "feature.upscaling.frame_generation_in_menus_tooltip_2": "可能感觉更流畅,但会增加菜单输入延迟。", - "feature.upscaling.frame_generation_proxy_note": "需要D3D11到D3D12代理,可能产生兼容性问题", - "feature.upscaling.frame_generation_restart_note": "切换此设置需要重启才能正常工作", - "feature.upscaling.frame_generation_tech": "使用AMD FSR帧生成技术", - "feature.upscaling.frame_limit_vrr": "帧率限制(可变刷新率)", - "feature.upscaling.method": "方法", - "feature.upscaling.streamline_logging_restart_note": "更改此设置需要重启才能生效。", - "feature.upscaling.streamline_logging_tooltip": "Streamline日志记录控制NVIDIA Streamline后端日志的详细程度。对于调试DLSS/DLSS-G问题很有用。", - "feature.upscaling.upscale_preset": "升频预设", - "feature.volumetric_lighting.enable_exteriors": "在室外启用体积光照", - "feature.vr.description": "为Community Shaders提供VR专用优化和增强,提升虚拟现实环境中的性能和视觉质量。", - "feature.vr.key_feature_1": "深度缓冲区剔除优化,提升VR性能", - "feature.vr.key_feature_2": "场景内叠加菜单,支持HMD/控制器/固定世界附着模式", - "feature.vr.key_feature_3": "VR控制器输入,可自定义按键映射", - "feature.vr.key_feature_4": "握持拖拽叠加层定位,带深度控制", - "feature.vr.key_feature_5": "可配置的遮挡剔除参数", - "feature.vr.key_feature_6": "增强的VR兼容性,支持SteamVR和OpenComposite", - "feature.vr.name": "VR", - "feature.vr_stereo.debug": "调试", - "feature.vr_stereo.debug_pom_depth": "调试POM深度", - "feature.vr_stereo.disocclusion_depth_threshold": "去遮挡深度阈值", - "feature.vr_stereo.enable": "启用", - "feature.vr_stereo.enable_stereo_reprojection": "启用立体重投影", - "feature.vr_stereo.enable_stereo_reprojection_tooltip": "使用深度和运动数据将眼0(左)像素重投影到眼1(右),\n在视图重叠处跳过冗余的完整着色。\n通过每帧对每个像素减少着色次数来降低VR中的GPU成本。", - "feature.vr_stereo.forward_occlusion_scale": "前向遮挡缩放", - "feature.vr_stereo.forward_occlusion_scale_tooltip": "防止眼0轮廓边缘渗到眼1背景上。\n当眼0深度在眼1深度的此比例内时触发(例如0.5 = 眼0小于2倍眼1深度)。\n更低 = 更激进。0 = 禁用。", - "feature.vr_stereo.full_blend_depth_view": "完全混合深度视图", - "feature.vr_stereo.full_blend_distance": "完全混合距离", - "feature.vr_stereo.full_blend_distance_tooltip": "比此距离(游戏单位)更近的几何体在双眼完全着色并双向混合以实现2倍超采样。0 = 禁用。", - "feature.vr_stereo.full_blend_zone_hint": " 青色 = 完全混合区域(越近色调越强)", - "feature.vr_stereo.off": "关闭", - "feature.vr_stereo.pom_depth_scale": "POM深度缩放", - "feature.vr_stereo.pom_depth_scale_tooltip": "立体重投影中POM深度校正的缩放因子。\n1.0 = 物理缩放。增加以获得更明显的POM立体深度。", - "feature.vr_stereo.restart_required": "需要重启才能启用VR立体重投影。", - "feature.vr_stereo.skip_pixel_reprojection": "跳过像素重投影", - "menu.advanced.active_shaders": "活跃着色器", - "menu.advanced.active_shaders_used_recently": "活跃着色器(近期使用)", - "menu.advanced.addresses": "地址", - "menu.advanced.avg_parallelism_metric": "平均并行度(W/S):%.2fx", - "menu.advanced.avg_parallelism_tooltip_1": "此工作负载中的平均有效并发度。", - "menu.advanced.avg_parallelism_tooltip_2": "大致为添加更多核心收益递减的工作线程数。", - "menu.advanced.avoid_flow_control": "避免流控制", - "menu.advanced.avoid_flow_control_tooltip": "向着色器编译器标志添加D3DCOMPILE_AVOID_FLOW_CONTROL。\n强制fxc将分支展平为谓词操作,而非发出动态流控制。对于短分支体和均匀采用的分支通常是优势;对于原版流控制会完全跳过的长分叉分支通常是劣势。\n每次启动时重置。切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.background_compiler_threads": "后台编译器线程", - "menu.advanced.compiler_threads": "编译器线程", - "menu.advanced.copy_key": "复制键", - "menu.advanced.dump_ini_settings": "导出INI设置", - "menu.advanced.efficiency_progress": "{:.1f}% 高效 / {:.1f}% 差距", - "menu.advanced.enable_file_watcher": "启用文件监视器", - "menu.advanced.enable_file_watcher_tooltip": "在文件更改时自动重新编译着色器。供开发使用。", - "menu.advanced.frame_annotations": "帧注释", - "menu.advanced.frame_annotations_tooltip": "启用详细的帧注释以调试渲染Pass和绘制调用。", - "menu.advanced.half_precision": "半精度(部分精度)", - "menu.advanced.half_precision_tooltip": "向着色器编译器标志添加D3DCOMPILE_PARTIAL_PRECISION。\n允许fxc将未标记的float操作降级为FP16,在其能证明安全的前提下,叠加现有的min16float类型提示。\n在支持FP16的GPU上(Pascal+/GCN+/Skylake+),可将寄存器压力减半并加倍ALU吞吐量,但对于未经过精度敏感性审查的着色器,也可能引入细微的视觉差异。\n切换此项将清除着色器缓存并触发完全重新编译。", - "menu.advanced.infinite_core_efficiency": "无限核心效率", - "menu.advanced.infinite_core_efficiency_metric": "无限核心效率(S/T_p):%.1f%%", - "menu.advanced.infinite_core_efficiency_tooltip_1": "运行时间与无限核心下限的接近程度。", - "menu.advanced.infinite_core_efficiency_tooltip_2": "100%%意味着T_p == S。", - "menu.advanced.infinite_core_gap_metric": "无限核心差距:%.1f%%", - "menu.advanced.infinite_core_gap_tooltip_1": "与理想无限核心时间的距离。", - "menu.advanced.infinite_core_gap_tooltip_2": "定义为100 * (1 - S / T_p)。越低越好。", - "menu.advanced.log_level": "日志级别", - "menu.advanced.log_level_critical": "严重", - "menu.advanced.log_level_debug": "调试", - "menu.advanced.log_level_err": "错误", - "menu.advanced.log_level_info": "信息", - "menu.advanced.log_level_off": "关闭", - "menu.advanced.log_level_tooltip": "日志级别。跟踪最详细。默认为信息。", - "menu.advanced.log_level_trace": "跟踪", - "menu.advanced.log_level_warn": "警告", - "menu.advanced.makespan_label": "完工时间(T_p)", - "menu.advanced.makespan_metric": "完工时间(T_p):%s", - "menu.advanced.makespan_tooltip": "完整着色器构建的观察到的墙上时钟时间。", - "menu.advanced.open_logs": "打开日志", - "menu.advanced.parallelism_header": "并行度(来自%zu个已编译任务)", - "menu.advanced.parallelism_tooltip_1": "从上一次完成的构建中惰性计算。", - "menu.advanced.parallelism_tooltip_2": "仅在此统计信息部分打开时评估。", - "menu.advanced.queue_wait_metric": "队列等待(平均/最大):%s / %s", - "menu.advanced.queue_wait_tooltip_1": "在就绪队列中工作线程开始编译前的等待时间。", - "menu.advanced.queue_wait_tooltip_2": "有助于识别与编译成本分离的调度器引起的延迟。", - "menu.advanced.relative_bar_format": "{}({:.1f}%)", - "menu.advanced.relative_durations": "相对持续时间(归一化)", - "menu.advanced.replace_original_shaders": "替换原始着色器", - "menu.advanced.shader_compiler_stats": "着色器编译器:{}", - "menu.advanced.shader_debug_header": "着色器调试", - "menu.advanced.shader_slow_entry": "#%zu %s (权重%d)", - "menu.advanced.span_label": "跨度(S)", - "menu.advanced.span_metric": "跨度(S,最长):%s", - "menu.advanced.span_tooltip_1": "关键路径下限,由最慢的单个着色器近似。", - "menu.advanced.span_tooltip_2": "即使无限核心也无法比这更快完成。", - "menu.advanced.statistics": "统计", - "menu.advanced.tab_developer": "开发者", - "menu.advanced.tab_disable_at_boot": "启动时禁用", - "menu.advanced.tab_logging": "日志记录", - "menu.advanced.tab_shader_debug": "着色器调试", - "menu.advanced.tab_testing": "测试", - "menu.advanced.top_slowest_shaders": "前%zu个最慢着色器(上次构建)", - "menu.advanced.work_label": "工作量(W)", - "menu.advanced.work_metric": "工作量(W,任务墙上时间总和):%s", - "menu.advanced.work_tooltip_1": "总编译工作量:所有单个着色器墙上时钟编译时间的总和。", - "menu.advanced.work_tooltip_2": "这不是CPU时间;是累积的任务经过时间。", - "menu.advanced.work_tooltip_3": "如果开销保持不变,单个工作线程上的等效串行时间。", - "menu.faq.a1": "Community Shaders是Skyrim的一个综合图形增强框架,提供高级光照、材质和视觉效果。它采用模块化设计,允许您仅启用所需的功能,同时保持良好的性能。", - "menu.faq.a6": "不,Community Shaders与ENB不兼容。如果检测到ENB,Community Shaders将自动禁用自身。", - "menu.faq.a7": "默认情况下,Community Shaders使用END键打开此菜单。如果您的键盘没有END键或该键无效,可以在通用 > 按键绑定选项卡中更改。您也可以在JSON配置文件中编辑热键。", - "menu.faq.a8": "我们一直在寻找有才华的开发者加入团队!请查看我们的GitHub wiki了解贡献指南,并加入我们的Discord服务器与开发团队联系。无论您对着色器编程、C++开发还是文档撰写感兴趣,总有可贡献的内容。", - "menu.faq.a9": "是的!Community Shaders完全开源,可在GitHub上获取。您可以查看源代码、报告问题、建议功能和为项目做出贡献。该项目采用GPL许可证,确保对所有人保持免费和开放。品牌材料和资产(图标、Nexus品牌、字体等)不受GPL许可证涵盖。任何包含的资产未经明确许可不得使用。", - "menu.faq.q1": "什么是Community Shaders?", - "menu.faq.q6": "Community Shaders与ENB兼容吗?", - "menu.faq.q7": "菜单热键无效!", - "menu.faq.q8": "我想帮助开发Community Shaders。", - "menu.faq.q9": "Community Shaders是开源的吗?", - "menu.home.dev_wiki": "开发者Wiki", - "menu.home.github": "GitHub", - "menu.home.intro": "Community Shaders为Skyrim提供高级图形增强。\n这套全面的功能集合带来现代渲染技术,提升您的视觉体验。", - "menu.home.join_discord": "加入我们的Discord", - "menu.home.nexus_mods": "Nexus Mods", - "menu.home.welcome": "欢迎使用Community Shaders {version}", - "menu.home.welcome_dev": "欢迎使用Community Shaders {version} [{build}]", - "menu.home.wiki": "Wiki", - "menu.issues.check_modified_files": "检查Data/Shaders/中的修改文件(不在功能子文件夹中)", - "menu.issues.compilation_persist_warning": "如果删除后编译问题仍然存在:", - "menu.issues.core_feature_installed_tooltip": "此功能已作为核心Community Shaders安装的一部分包含。请通过模组管理器卸载此功能。", - "menu.issues.reinstall_cs": "如果问题持续存在,请考虑重新安装Community Shaders", - "menu.issues.test.active_inis_warning": "测试INI文件当前处于活动状态。重启CS以查看功能问题。", - "menu.issues.test.create_test_inis": "创建测试INI", - "menu.issues.test.create_test_inis_tooltip": "创建触发所有已知功能问题情况的测试INI文件:\n- 过时功能(ComplexParallaxMaterials、TerrainBlending等)\n- 未知功能(伪造的、不存在的功能)\n- 版本不匹配(修改现有功能版本)\n创建后重启CS以查看问题运作。", - "menu.issues.test.feature_issue_testing": "功能问题测试", - "menu.issues.test.feature_issue_testing_desc": "这些工具创建测试INI文件,以触发所有已知功能问题类型,供测试目的使用。", - "menu.issues.test.restore": "恢复", - "menu.issues.test.restore_tooltip": "删除所有测试INI文件并将任何修改过的INI文件恢复到原始状态。\n这将撤销\"创建测试INI\"所做的所有更改。\n恢复后重启CS以查看正常操作。", - "menu.issues.test.testing_header": "测试", - "menu.issues.uninstall_via_mod_manager": "通过模组管理器完全卸载功能", - "menu.setup.new_install_line1": "这似乎是一个新的安装、更新,或", - "menu.setup.new_install_line2": "重新安装Community Shaders。", - "menu.window_title": "Community Shaders {version}", - "menu.window_title_dev": "Community Shaders {version} [{build}]" + "ui.search_features": "搜索功能..." }