diff --git a/.github/workflows/pr-i18n.yaml b/.github/workflows/pr-i18n.yaml new file mode 100644 index 0000000000..3279da9005 --- /dev/null +++ b/.github/workflows/pr-i18n.yaml @@ -0,0 +1,95 @@ +name: "PR: i18n Check" + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "src/**" + - "package/SKSE/Plugins/CommunityShaders/Translations/**" + - "tools/extract-i18n.py" + - "tools/sort-i18n.py" + +permissions: + contents: read + +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 + 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: Check translation file key order matches en.json + run: python tools/sort-i18n.py --check + + - 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/CMakeLists.txt b/CMakeLists.txt index 113391c6b2..9ad14faaca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,10 +143,13 @@ 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} ${EXPRTK_INCLUDE_DIRS} ) +target_compile_definitions(${PROJECT_NAME} PRIVATE SK_HDR_PNG_COMMUNITY_SHADERS) + target_link_libraries( ${PROJECT_NAME} PRIVATE @@ -166,6 +169,7 @@ target_link_libraries( d3d12.lib Microsoft::DirectX-Headers ${DETOURS_LIBRARY} + windowscodecs ) # devbench bridge: opt-out via -DDEVBENCH_BRIDGE=OFF. When off, the port isn't required @@ -184,6 +188,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/TRANSLATING.md b/TRANSLATING.md new file mode 100644 index 0000000000..83e2c2203c --- /dev/null +++ b/TRANSLATING.md @@ -0,0 +1,138 @@ +# 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 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 + +``` +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/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/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..8e7cd350f1 --- /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/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/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli index 515c440586..c22c603610 100644 --- a/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli +++ b/features/Dynamic Cubemaps/Shaders/DynamicCubemaps/DynamicCubemaps.hlsli @@ -41,81 +41,87 @@ 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) { - 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, skySpecular; - - 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)); + 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); + } +# 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); - } 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); +# endif } -# 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); + } else { +# if defined(IBL) && defined(LIGHTING) + float3 specularIrradiance = ImageBasedLighting::StaticSpecularIBLTexture.SampleLevel(SampColorSampler, R.xzy, level).xyz; + finalIrradiance = specularIrradiance; # 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 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/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli b/features/Exponential Height Fog/Shaders/ExponentialHeightFog/ExponentialHeightFog.hlsli index c2e6bba23f..c76d7295c7 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,214 @@ 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) + { + // 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) + { + projectedDepth = GetSceneDepthFromClip(clipPosition); + volumeUV = saturate(clipPosition.xy / clipPosition.w * float2(0.5f, -0.5f) + 0.5f); + } + + 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 = 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); + +#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 = 0.0f.xx; + float projectedDepth = 0.0f; + 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 +261,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/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 d2f4410bac..51c56ac12c 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/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/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/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 0000000000..3293c36ef3 Binary files /dev/null and b/features/Skin/Shaders/Skin/skin_detail_n.dds differ 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..efa84cac75 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 +Version = 1-2-0 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/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/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 Helper/Shaders/Features/TerrainHelper.ini b/features/Terrain Helper/Shaders/Features/TerrainHelper.ini index 9194f78993..321113bad1 100644 --- a/features/Terrain Helper/Shaders/Features/TerrainHelper.ini +++ b/features/Terrain Helper/Shaders/Features/TerrainHelper.ini @@ -6,4 +6,4 @@ AuditVersion = false nexusmodid = 143149 nexusfilegroupid = 000000 nexusfilename = Terrain Helper -autoupload = true +autoupload = false 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/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 91644e0c4c..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,25 +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; } } 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/include/DynamicWetness_PublicAPI.h b/include/DynamicWetness_PublicAPI.h new file mode 100644 index 0000000000..e711db7540 --- /dev/null +++ b/include/DynamicWetness_PublicAPI.h @@ -0,0 +1,350 @@ +// Public, header-only C/C++ interface for DynamicWetness (SWE namespace) +// Drop-in for other SKSE plugins, no import lib required (functions are resolved via GetProcAddress). +// +// Quick start: +// #include "DynamicWetness_PublicAPI.h" +// bool ok = SWE::API::Init(); // resolves exports from DynamicWetness.dll +// if (!ok) return; // SWE not present +// SWE::API::SetExternalWetness(actor, "MyMod:buff", 0.5f, 8.0f); // 8s light wetness on default category (Skin) +// auto env = SWE::API::DecodeEnv(SWE::API::GetEnvMask(actor)); // query environment (water, rain, roof, heat, +// ...) +// +// Notes: +// - All intensities are clamped to [0..1]. +// - Durations use seconds, <= 0 means "indefinite until cleared". +// - Category mask low 4 bits select target materials, high bits are behavior flags. +// - Keys are normalized (trim + lowercase) and identify your external source per actor. +// - Environmental wetness (water/rain) can override external sources internally. +// - Thread-safe internally, but always pass valid Actor* (lifetime: game thread best). + +#pragma once +#include + +#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/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/package/SKSE/Plugins/CommunityShaders/Translations/en.json b/package/SKSE/Plugins/CommunityShaders/Translations/en.json new file mode 100644 index 0000000000..e0888b1004 --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Translations/en.json @@ -0,0 +1,1954 @@ +{ + "_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.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_information": "Weather Information", + "feature.cs_editor.weather_percentage": "Weather Percentage: %.1f%%", + "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)", + "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.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.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.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", + "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 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.", + "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.name": "Light Limit Fix", + "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.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.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.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.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_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 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.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.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.", + "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", + "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.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.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.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_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.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_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.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.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", + "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_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.", + "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_system_state": "Rain System State", + "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_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.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_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.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.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.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.shader_blocking_active": "Shader Blocking Active", + "menu.advanced.shader_class_label": "Class: %s", + "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_type_label": "Type: %s", + "menu.advanced.stop_blocking": "Stop Blocking##Section", + "menu.advanced.test_conditions": "Test Conditions", + "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.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.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.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.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.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", + "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.quick_links": "Quick Links", + "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.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.core_feature_installed": "Core feature already installed", + "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.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.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.this_will_delete": "This will delete:", + "menu.issues.time_label": "Time: %s", + "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.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", + "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 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.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 '", + "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 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", + "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_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", + "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", + "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.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.", + "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..70e24bdd9c --- /dev/null +++ b/package/SKSE/Plugins/CommunityShaders/Translations/zh_CN.json @@ -0,0 +1,1939 @@ +{ + "_meta": { + "language": "简体中文", + "locale": "zh_CN", + "version": "1.0.0", + "authors": "Jiaye" + }, + "common.active": "激活", + "common.inactive": "未激活", + "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.close_cs_editor": "关闭 CS 编辑器(Esc)", + "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.cs_editor": "CS 编辑器", + "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.parent_cs_editor_feature": "仅编辑器功能:设置父级天气以从中复制设置。", + "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.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": "极光", + "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.description": "用于在游戏内检查、编辑和预览面向渲染器数据的开发工具。", + "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.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)", + "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.name": "CS 编辑器", + "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.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_information": "天气信息", + "feature.cs_editor.weather_percentage": "天气百分比:%.1f%%", + "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)", + "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° = 逆风(风朝向玩家)", + "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.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.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.name": "光源限制修复", + "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.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.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.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.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_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.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.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.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": "强度", + "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.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.fps_limit": "FPS限制", + "feature.upscaling.fps_limit_tooltip_1": "设置帧率上限目标。", + "feature.upscaling.fps_limit_tooltip_2": "起始值设置为比刷新率低2-3 FPS(例如120 Hz为117)。", + "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_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.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_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.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_tooltip": "最近帧中使用过的着色器列表。在上方启用着色器拦截可使用热键循环浏览并拦截着色器进行调试。约1秒未使用的着色器将从此列表中移除。", + "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_tooltip": "启动时用于编译着色器的线程数。默认为所有逻辑核心减去一个以留出系统开销(包含E核)。较高值可更快完成编译,但可能降低系统响应性。", + "menu.advanced.compute": "计算", + "menu.advanced.compute_tooltip": "替换计算着色器。设为false时将禁用上述类型的自定义计算着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.advanced.copy_info": "复制信息", + "menu.advanced.copy_info_tooltip": "将包含缓存路径的完整着色器信息复制到剪贴板", + "menu.advanced.dump_shaders": "导出着色器", + "menu.advanced.dump_shaders_tooltip": "在启动时导出着色器。仅在逆向着色器时使用。普通用户无需此功能。", + "menu.advanced.enable_shader_blocking": "启用着色器拦截", + "menu.advanced.enable_shader_blocking_tooltip": "启用热键以循环浏览并拦截单个着色器,用于调试目的。", + "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.shader_blocking_active": "着色器拦截已激活", + "menu.advanced.shader_class_label": "类别:%s", + "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_type_label": "类型:%s", + "menu.advanced.stop_blocking": "停止拦截##Section", + "menu.advanced.test_conditions": "测试条件", + "menu.advanced.vertex": "顶点", + "menu.advanced.vertex_tooltip": "替换顶点着色器。设为false时将禁用上述类型的自定义顶点着色器。供开发者测试CS着色器是否与原版行为匹配。", + "menu.clear_shader_cache": "清除着色器缓存", + "menu.clear_shader_cache_tooltip": "清除着色器缓存和磁盘缓存(如果启用)。\n着色器缓存是在运行时替换原版着色器的已编译着色器集合。\n磁盘缓存是磁盘上已编译着色器的集合。清除后意味着着色器仅在游戏再次遇到它们时才重新编译。", + "menu.disable_at_boot_desc": "选择要在启动时禁用的功能。这与删除feature.ini文件相同。重新启用需要重启。", + "menu.faq.a2": "每个功能都可以在左侧边栏菜单中找到。点击任何功能即可访问其设置。大多数功能包含预设和详细的工具提示,帮助您了解每个设置的作用。", + "menu.faq.a3": "功能可能因硬件不兼容、依赖项缺失或与其他模组冲突而无法加载。请查看\"功能问题\"选项卡,了解有关任何有问题的功能的详细信息。", + "menu.faq.a4": "着色器失败通常由混合文件版本引起。请确保所有功能均为最新,并避免混合测试版本或过时版本的文件。请查看\"功能问题\"选项卡和/或Wiki了解更多信息。更新您的功能并移除任何过时的功能。", + "menu.faq.a5": "首先启用性能叠加层来监控您的FPS。考虑禁用屏幕空间GI等占用资源的功能或降低质量设置。\"显示\"选项卡还包含可以提升性能的升频选项。", + "menu.faq.q2": "如何配置功能?", + "menu.faq.q3": "为什么有些功能无法加载?", + "menu.faq.q4": "编译时出现“着色器失败”?", + "menu.faq.q5": "如何提升性能?", + "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.quick_links": "快速链接", + "menu.issues.all_ini_loading": "所有功能INI文件加载成功。", + "menu.issues.cancel": "取消", + "menu.issues.cannot_be_undone": "此操作无法撤销!", + "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.core_feature_installed": "核心功能已安装", + "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.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.modified_notice": "\n部分测试文件已修改 - 建议恢复以清理", + "menu.issues.test.no_active_inis": "当前没有活跃的测试INI文件。", + "menu.issues.this_will_delete": "这将删除:", + "menu.issues.time_label": "时间:%s", + "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.press_any_key": "按下任意键设置为切换键...", + "menu.setup.press_to_close": "按Escape或Enter继续", + "menu.toggle_error_message": "切换错误消息", + "menu.toggle_error_message_tooltip": "隐藏或显示着色器失败消息。您的安装已损坏,游戏中可能会看到错误。请仔细检查是否已更新所有功能以及加载顺序是否正确。请参阅CommunityShaders.log了解详情,并查看Nexus Mods页面或Discord服务器。", + "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/package/Shaders/Common/FrameBuffer.hlsli b/package/Shaders/Common/FrameBuffer.hlsli index 775d41f2d6..68f0f90371 100644 --- a/package/Shaders/Common/FrameBuffer.hlsli +++ b/package/Shaders/Common/FrameBuffer.hlsli @@ -85,6 +85,9 @@ namespace FrameBuffer return clamp(screenPositionDR, minValue, maxValue); } + // 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) { float4 positionCS = mul(FrameBuffer::CameraViewProj[eyeIndex], float4(positionWS, 1)); 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/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/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index b33cd6aba0..acc7bb2f9d 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; @@ -279,7 +285,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 @@ -288,6 +313,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; @@ -307,6 +343,7 @@ namespace SharedData TerrainBlendingSettings terrainBlendingSettings; ExponentialHeightFogSettings exponentialHeightFogSettings; TruePBRSettings truePBRSettings; + SkinData skinData; }; Texture2D DepthTexture : register(t17); 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; } } 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/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 f95210c543..8d19642f03 100644 --- a/package/Shaders/Effect.hlsl +++ b/package/Shaders/Effect.hlsl @@ -934,16 +934,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; } } @@ -964,6 +966,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/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 diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 766cd25fd7..65a506be48 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 @@ -550,6 +548,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 @@ -927,6 +931,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" @@ -1332,6 +1340,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; @@ -1869,6 +1888,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) @@ -1931,6 +1991,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; @@ -2026,6 +2092,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 + float projectedMaterialWeight = 0; float projWeight = 0; @@ -2052,9 +2171,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; @@ -2083,18 +2199,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 } } @@ -2104,13 +2214,7 @@ 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 +# endif // SNOW # if defined(WORLD_MAP) baseColor.xyz = GetWorldMapBaseColor(rawBaseColor.xyz, baseColor.xyz, projWeight); @@ -2269,6 +2373,38 @@ 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(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; @@ -2428,6 +2564,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); @@ -2435,7 +2584,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; @@ -2464,6 +2613,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 @@ -3009,13 +3165,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; @@ -3028,6 +3185,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) @@ -3227,12 +3385,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()) { @@ -3418,16 +3581,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) @@ -3436,6 +3589,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 727a51580e..d9edfd0aee 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 @@ -641,7 +640,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) @@ -649,7 +649,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 @@ -804,7 +803,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); @@ -812,6 +810,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; } @@ -928,7 +927,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) @@ -936,7 +936,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 @@ -978,6 +977,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/package/Shaders/Sky.hlsl b/package/Shaders/Sky.hlsl index a4c83b2f03..1d4be8643c 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 @@ -191,6 +193,11 @@ cbuffer AlphaTestRefCB : register(b11) # include "CloudShadows/CloudShadows.hlsli" # endif +# if defined(EXP_HEIGHT_FOG) +# define SampColorSampler SampBaseSampler +# include "ExponentialHeightFog/ExponentialHeightFog.hlsli" +# endif + # ifdef HDR_OUTPUT # include "HDRDisplay/HDRSun.hlsli" # include "Common/Random.hlsli" @@ -282,6 +289,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/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 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/WeatherEditor/EditorWindow.cpp b/src/CSEditor/EditorWindow.cpp similarity index 81% rename from src/WeatherEditor/EditorWindow.cpp rename to src/CSEditor/EditorWindow.cpp index 9eb12a0fc5..fce755896e 100644 --- a/src/WeatherEditor/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/WeatherEditor.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) @@ -90,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; @@ -117,7 +116,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 +147,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 +197,7 @@ std::string EditorWindow::ResolveEditorId(RE::TESForm* form, const WidgetVec& wi void EditorWindow::ShowObjectsWindow() { - Util::BeginWithRoundedClose("Weather and Lighting 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 +210,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,17 +224,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" }; + 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(); @@ -238,6 +270,16 @@ void EditorWindow::ShowObjectsWindow() return; } + if (m_selectedCategory == "Light 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>& { @@ -315,7 +357,7 @@ 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); @@ -328,7 +370,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) { @@ -368,7 +410,9 @@ void EditorWindow::ShowObjectsWindow() if (!activeRecords.empty()) { const auto& theme = Menu::GetSingleton()->GetTheme(); - Util::Text::RestartNeeded("Active:"); + ImGui::PushStyleColor(ImGuiCol_Text, theme.StatusPalette.RestartNeeded); + ImGui::Text("%s", T(TKEY("active"), "Active:")); + ImGui::PopStyleColor(); ImGui::SameLine(); const float recordX = ImGui::GetCursorPosX(); @@ -387,7 +431,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(); } @@ -413,26 +457,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); @@ -443,7 +496,7 @@ void EditorWindow::ShowObjectsWindow() m_showOnlyFavorites = !m_showOnlyFavorites; } ImGui::SameLine(); - ImGui::Text("Favorites"); + ImGui::Text("%s", favoritesText); ImGui::SameLine(); ImGui::Dummy(filterSpacer); @@ -452,13 +505,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) @@ -492,12 +545,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(); @@ -594,7 +647,7 @@ void EditorWindow::ShowObjectsWindow() pendingDeleteWidget = widget; pendingDeletePopupRequested = true; } - Util::AddTooltip("Delete JSON file"); + Util::AddTooltip(T(TKEY("delete_json_file"), "Delete JSON file")); } } }; @@ -660,7 +713,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(); @@ -669,8 +722,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 { @@ -678,7 +731,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(); } } @@ -743,19 +796,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(); } @@ -780,7 +833,7 @@ void EditorWindow::ShowObjectsWindow() } } - if (ImGui::MenuItem("Remove")) { + if (ImGui::MenuItem(T(TKEY("remove"), "Remove"))) { markedRecords.erase(editorLabel); Save(); } @@ -840,7 +893,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(); @@ -863,7 +916,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(); @@ -918,13 +971,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); @@ -936,7 +989,7 @@ void EditorWindow::RenderUI() } if (!hasOpen) - ImGui::TextDisabled("No open widgets"); + ImGui::TextDisabled("%s", T(TKEY("no_open_widgets"), "No open widgets")); ImGui::EndMenu(); } @@ -946,12 +999,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"; } @@ -960,7 +1013,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) { @@ -978,45 +1031,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)) { - BackgroundBlur::SetWeatherEditorActive(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()) @@ -1024,47 +1077,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("Weather 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(); } @@ -1090,7 +1143,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 @@ -1147,13 +1200,13 @@ 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); + 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; } @@ -1161,7 +1214,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; @@ -1173,7 +1226,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; } @@ -1229,20 +1282,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 @@ -1259,7 +1312,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(T(TKEY("close_cs_editor"), "Close CS Editor (Esc)")); ImGui::PopClipRect(); // End bottom-border clip rect @@ -1406,12 +1459,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; @@ -1419,12 +1473,15 @@ 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; const bool viewportActive = IsViewportActive(); if (viewportActive != prevViewportActive) { - BackgroundBlur::SetWeatherEditorActive(viewportActive); + BackgroundBlur::SetCSEditorActive(viewportActive); prevViewportActive = viewportActive; } } @@ -1514,72 +1571,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; @@ -1626,7 +1691,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; } } @@ -1671,7 +1737,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(); } @@ -1865,18 +1931,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) @@ -1885,22 +1954,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() @@ -1950,7 +2019,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"); } } @@ -1962,7 +2031,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"); } } @@ -2086,7 +2155,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); } @@ -2233,3 +2302,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/WeatherEditor/EditorWindow.h b/src/CSEditor/EditorWindow.h similarity index 99% rename from src/WeatherEditor/EditorWindow.h rename to src/CSEditor/EditorWindow.h index e53c3cc8a1..c4856ef2e7 100644 --- a/src/WeatherEditor/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" @@ -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/WeatherEditor/InteriorOnlyPanel.cpp b/src/CSEditor/InteriorOnlyPanel.cpp similarity index 82% rename from src/WeatherEditor/InteriorOnlyPanel.cpp rename to src/CSEditor/InteriorOnlyPanel.cpp index 0c40d0d5d1..bd40c0f55d 100644 --- a/src/WeatherEditor/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/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/Features/InverseSquareLighting/LightEditor.cpp b/src/CSEditor/LightEditor.cpp similarity index 79% rename from src/Features/InverseSquareLighting/LightEditor.cpp rename to src/CSEditor/LightEditor.cpp index abcfdb0a11..8c6f5fa899 100644 --- a/src/Features/InverseSquareLighting/LightEditor.cpp +++ b/src/CSEditor/LightEditor.cpp @@ -1,7 +1,10 @@ -#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 "../I18n/I18n.h" +#include "../Menu.h" + +#define I18N_KEY_PREFIX "feature.light_editor." #include #include @@ -10,47 +13,32 @@ 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); + 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; @@ -72,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; @@ -94,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.")); } } @@ -106,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) @@ -168,10 +158,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 +275,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 +367,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 +702,4 @@ void LightEditor::SortLights() default: break; } -} \ No newline at end of file +} diff --git a/src/Features/InverseSquareLighting/LightEditor.h b/src/CSEditor/LightEditor.h similarity index 94% rename from src/Features/InverseSquareLighting/LightEditor.h rename to src/CSEditor/LightEditor.h index 1748fa34fd..48be6b33c7 100644 --- a/src/Features/InverseSquareLighting/LightEditor.h +++ b/src/CSEditor/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; diff --git a/src/WeatherEditor/PaletteWindow.cpp b/src/CSEditor/PaletteWindow.cpp similarity index 80% rename from src/WeatherEditor/PaletteWindow.cpp rename to src/CSEditor/PaletteWindow.cpp index 905e3ff4c2..7c08c79982 100644 --- a/src/WeatherEditor/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/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 93% rename from src/WeatherEditor/Weather/CellLightingWidget.cpp rename to src/CSEditor/Weather/CellLightingWidget.cpp index 969a98419d..2c2ad62727 100644 --- a/src/WeatherEditor/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; @@ -78,13 +81,13 @@ void CellLightingWidget::DrawWidget() 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) { @@ -99,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); }); @@ -109,13 +112,13 @@ 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 ImGui::SliderInt(std::format("{}##{}", T(TKEY("xy_rotation"), "XY Rotation"), CellLightingSetting::kXYRotation).c_str(), &xyDegrees, 0, 360); }); }); })) { @@ -126,7 +129,7 @@ void CellLightingWidget::DrawWidget() if (DrawIfMatchesSearch(CellLightingSetting::kZRotation, [&](const char* label) { return drawInherited(settings.inheritDirectionalRotation, [&]() { return DrawWithHighlight(label, [&]() { - return ImGui::SliderInt(label, &zDegrees, 0, 360); + return ImGui::SliderInt(std::format("{}##{}", T(TKEY("z_rotation"), "Z Rotation"), CellLightingSetting::kZRotation).c_str(), &zDegrees, 0, 360); }); }); })) { @@ -139,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); }); @@ -149,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); }); @@ -159,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*) { @@ -208,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); }); @@ -223,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); }); @@ -248,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); @@ -587,8 +590,10 @@ std::vector CellLightingWidget::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; } + +#undef I18N_KEY_PREFIX 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 92% rename from src/WeatherEditor/Weather/LightingTemplateWidget.cpp rename to src/CSEditor/Weather/LightingTemplateWidget.cpp index a9f830ffb1..ac9c49382c 100644 --- a/src/WeatherEditor/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(); @@ -334,7 +339,7 @@ std::vector LightingTemplateWidget::CollectSearchableSetti 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/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 88% rename from src/WeatherEditor/Weather/PrecipitationWidget.cpp rename to src/CSEditor/Weather/PrecipitationWidget.cpp index 62aec63784..8efa1d915a 100644 --- a/src/WeatherEditor/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,14 +52,14 @@ 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" }; + 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(label, ¤tType, types, IM_ARRAYSIZE(types)); + 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); @@ -64,12 +69,12 @@ void PrecipitationWidget::DrawWidget() })) 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,15 +99,15 @@ 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 ImGui::InputInt(std::format("{}##{}", T(TKEY("num_subtextures_x"), "Num Subtextures X"), PrecipitationSetting::kNumSubtexturesX).c_str(), &numX); }); })) { settings.numSubtexturesX = std::max(1, numX); @@ -110,7 +115,7 @@ void PrecipitationWidget::DrawWidget() } if (DrawIfMatchesSearch(PrecipitationSetting::kNumSubtexturesY, [&](const char* label) { return DrawWithHighlight(label, [&]() { - return ImGui::InputInt(label, &numY); + return ImGui::InputInt(std::format("{}##{}", T(TKEY("num_subtextures_y"), "Num Subtextures Y"), PrecipitationSetting::kNumSubtexturesY).c_str(), &numY); }); })) { settings.numSubtexturesY = std::max(1, numY); @@ -118,9 +123,9 @@ void PrecipitationWidget::DrawWidget() } } 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/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 79% rename from src/WeatherEditor/Weather/ReferenceEffectWidget.cpp rename to src/CSEditor/Weather/ReferenceEffectWidget.cpp index bffbd38038..f1b38e797d 100644 --- a/src/WeatherEditor/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/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 66% rename from src/WeatherEditor/Weather/SimpleFormWidget.h rename to src/CSEditor/Weather/SimpleFormWidget.h index 6bad8154a8..98efd67d4c 100644 --- a/src/WeatherEditor/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/WeatherEditor/Weather/VolumetricLightingWidget.cpp b/src/CSEditor/Weather/VolumetricLightingWidget.cpp similarity index 77% rename from src/WeatherEditor/Weather/VolumetricLightingWidget.cpp rename to src/CSEditor/Weather/VolumetricLightingWidget.cpp index f032a7ef8d..8f24921c3e 100644 --- a/src/WeatherEditor/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/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 80% rename from src/WeatherEditor/Weather/WeatherWidget.cpp rename to src/CSEditor/Weather/WeatherWidget.cpp index bd2d131203..19080c838e 100644 --- a/src/WeatherEditor/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; }; @@ -319,10 +355,10 @@ void WeatherWidget::DrawWidget() settings.inheritFlags[inheritKey] = false; } if (isInherited) { - Util::AddTooltip("Inherited from parent weather"); + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); PopInheritedStyle(); } - drawOpenButton(recordRefs[i], widgets, std::format("Open##{}", i), openTooltip); + drawOpenButton(recordRefs[i], widgets, std::format("{}##{}", T(TKEY("open"), "Open"), i), openTooltip); if (recordHighlighted) PopHighlightIfNeeded(rowId, recordHighlighted); @@ -330,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()) @@ -350,7 +386,7 @@ void WeatherWidget::DrawWidget() settings.inheritFlags[inheritKey] = false; } if (isInherited) { - Util::AddTooltip("Inherited from parent weather"); + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); PopInheritedStyle(); } drawOpenButton(recordRef, widgets, buttonId, openTooltip); @@ -364,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(); @@ -851,25 +887,25 @@ void WeatherWidget::DrawDALCSettings() }; 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; @@ -880,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; @@ -1035,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); @@ -1093,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; @@ -1155,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; } }); @@ -1210,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); @@ -1232,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(); } @@ -1255,7 +1291,7 @@ void WeatherWidget::DrawFogSlider(const char* id, float& prop, float min, float inheritRef = false; } if (isInherited) { - Util::AddTooltip("Inherited from parent weather"); + Util::AddTooltip(T(TKEY("inherited_from_parent_weather"), "Inherited from parent weather")); PopInheritedStyle(); } } @@ -1324,6 +1360,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapGetName(); + std::string displayName = feature->GetDisplayName(); auto featureIt = settings.featureSettings.find(featureName); const json* featureJsonView = (featureIt != settings.featureSettings.end()) ? &featureIt->second : nullptr; auto getFeatureJson = [&]() -> json& { @@ -1795,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; @@ -1804,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.")); } } @@ -1904,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; } @@ -1927,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; } @@ -1949,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; } @@ -1971,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; } @@ -1982,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.")); } } @@ -2000,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(); @@ -2028,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); @@ -2059,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; } @@ -2089,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/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 79% rename from src/WeatherEditor/WeatherUtils.cpp rename to src/CSEditor/WeatherUtils.cpp index 93ce82fd85..7e6ce65c7a 100644 --- a/src/WeatherEditor/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/WeatherEditor/WeatherUtils.h b/src/CSEditor/WeatherUtils.h similarity index 90% rename from src/WeatherEditor/WeatherUtils.h rename to src/CSEditor/WeatherUtils.h index 8b238a9a16..f580f96c0c 100644 --- a/src/WeatherEditor/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/WeatherEditor/Widget.cpp b/src/CSEditor/Widget.cpp similarity index 84% rename from src/WeatherEditor/Widget.cpp rename to src/CSEditor/Widget.cpp index 0f34fae9e8..693fd61f9d 100644 --- a/src/WeatherEditor/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/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 f6897389af..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/WeatherEditor.h" #include "Hooks.h" @@ -110,10 +110,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); - - // 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); + // Masks2 (vertexAO; fp16 to allow blending) + SetupRenderTarget(MASKS2, texDesc, srvDesc, rtvDesc, uavDesc, DXGI_FORMAT_R16_UNORM, D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE); } { @@ -248,7 +246,7 @@ void Deferred::StartDeferred() SPECULAR, REFLECTANCE, MASKS, - RE::RENDER_TARGET::kNONE + MASKS2 }; for (uint i = 2; i < 8; i++) { @@ -322,6 +320,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]]; @@ -366,7 +365,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 @@ -396,7 +395,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 @@ -416,7 +417,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/Feature.cpp b/src/Feature.cpp index 339afac16d..7bdf647aeb 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" @@ -23,6 +24,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" @@ -36,8 +38,8 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/WeatherEditor.h" #include "Features/WetnessEffects.h" +#include "I18n/I18n.h" #include "Menu.h" #include "SettingsOverrideManager.h" #include "Utils/Format.h" @@ -242,12 +244,13 @@ const std::vector& Feature::GetFeatureList() &globals::features::upscaling, &globals::features::renderDoc, &globals::features::remoteControl, - &globals::features::weatherEditor, + &globals::features::csEditor, &globals::features::screenshotFeature, &globals::features::linearLighting, &globals::features::unifiedWater, &globals::features::exponentialHeightFog, - &globals::features::hdrDisplay + &globals::features::hdrDisplay, + &globals::features::skin }; if (globals::game::isVR) { @@ -387,6 +390,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 @@ -417,7 +447,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 65c469013b..633a26fa1b 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" #include "Utils/RestartSettings.h" #include @@ -66,6 +67,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 {}; } @@ -107,6 +110,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/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 98f5aa834b..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" @@ -11,6 +13,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" @@ -21,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) @@ -53,5 +59,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/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/FeatureIssues.cpp b/src/FeatureIssues.cpp index 2f18b4d128..71a08b5407 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,7 +614,7 @@ 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 Open Shaders installation. Uninstall this feature with your mod manager."); } @@ -597,9 +622,18 @@ namespace FeatureIssues 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 +641,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 +680,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,22 +711,26 @@ 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"); @@ -682,22 +741,22 @@ namespace FeatureIssues 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 +768,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 +987,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 +1042,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 +1392,7 @@ namespace FeatureIssues if (outFile.fail()) { throw std::runtime_error("Failed to write file contents"); } + TestIniInfo testInfo; testInfo.testIniPath = iniPath.string(); testInfo.isNewFile = true; diff --git a/src/Features/WeatherEditor.cpp b/src/Features/CSEditor.cpp similarity index 68% rename from src/Features/WeatherEditor.cpp rename to src/Features/CSEditor.cpp index 2272b870a6..485386f731 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/CSEditor.cpp @@ -1,4 +1,7 @@ -#include "WeatherEditor.h" +#include "CSEditor.h" +#include "I18n/I18n.h" + +#define I18N_KEY_PREFIX "feature.cs_editor." #include "Deferred.h" #include "Feature.h" @@ -9,9 +12,10 @@ #include "Utils/UI.h" #include "WeatherManager.h" -#include "WeatherEditor/EditorWindow.h" +#include "CSEditor/EditorWindow.h" #include #include +#include #include namespace @@ -20,18 +24,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 +46,10 @@ 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()); + // 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) @@ -52,17 +59,18 @@ 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) { + logger::info("[CSEditor] Detected widget settings in '{}'", widgetSettingsPath.string()); s_hasWidgetJsonFiles = true; s_checkedWidgetJsonFiles = true; return true; } } 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 +79,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 +92,7 @@ void WeatherEditor::EnsureWeatherListLoaded() LoadAllWeathers(); } -void WeatherEditor::EnsureDataLoaded() +void CSEditor::EnsureDataLoaded() { if (!s_dataAvailable) return; @@ -96,7 +104,7 @@ void WeatherEditor::EnsureDataLoaded() LoadAllWeathers(); } -void WeatherEditor::OpenEditorWindow() +void CSEditor::OpenEditorWindow() { if (!EditorWindow::CanBeOpen()) return; @@ -105,7 +113,7 @@ void WeatherEditor::OpenEditorWindow() EditorWindow::GetSingleton()->open = true; } -void WeatherEditor::ToggleEditorWindow() +void CSEditor::ToggleEditorWindow() { auto* editorWindow = EditorWindow::GetSingleton(); if (!editorWindow) @@ -154,26 +162,50 @@ 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(T(TKEY("open_editor"), "Open CS Editor"), { -1, 0 })) OpenEditorWindow(); ImGui::EndDisabled(); + ImGui::Spacing(); + ImGui::SeparatorText(T(TKEY("weather_picker"), "Weather Picker")); + // Time controls DrawTimeControls(); - // Basic weather editor info + // Basic CS editor info DrawWeatherStatusPanel(); // 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 WeatherEditor::Prepass() +void CSEditor::Prepass() { if (ShouldPreloadEditorResources()) { EnsureDataLoaded(); @@ -193,35 +225,18 @@ void WeatherEditor::Prepass() editorWindow->UpdateTimeState(); } -void WeatherEditor::DrawWeatherPickerSection() +void CSEditor::DrawWeatherPickerSection() { ImGui::Spacing(); - Util::DrawSectionHeader("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)) { - 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::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(); } -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,20 +309,16 @@ void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newW } } -void WeatherEditor::DrawTimeControls() +void CSEditor::DrawTimeControls() { - ImGui::Spacing(); - Util::DrawSectionHeader("Time Controls"); ImGui::Spacing(); EditorWindow::GetSingleton()->DrawTimeControls(); ImGui::Spacing(); } -void WeatherEditor::DrawWeatherStatusPanel() +void CSEditor::DrawWeatherStatusPanel() { ImGui::Spacing(); - Util::DrawSectionHeader("Weather Status"); - ImGui::Spacing(); auto weatherManager = WeatherManager::GetSingleton(); auto currentWeathers = weatherManager->GetCurrentWeathers(); @@ -316,25 +327,25 @@ void WeatherEditor::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 +357,19 @@ void WeatherEditor::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")); } } @@ -364,7 +377,7 @@ void WeatherEditor::DrawWeatherStatusPanel() // Weather Picker functionality (integrated from WeatherPicker feature) // ================================================================================ -void WeatherEditor::RenderWeatherDetailsWindow(bool* open) +void CSEditor::RenderWeatherDetailsWindow(bool* open, bool showSectionHeaders) { if (!open || !*open) return; @@ -385,7 +398,7 @@ void WeatherEditor::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) { @@ -398,7 +411,7 @@ void WeatherEditor::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(); @@ -406,7 +419,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,67 +453,67 @@ 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"); + ImGui::BulletText("%s", T(TKEY("no_weather_found"), "No Weather Found")); return; } 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); + 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 WeatherEditor::DisplayPrecipitationInfo(RE::TESWeather* weather) +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") }); } } -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; @@ -508,7 +521,7 @@ void WeatherEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInter 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,118 +539,124 @@ void WeatherEditor::DisplayLightningInfo(RE::TESWeather* weather, bool showInter 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") }); } } -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))) return; 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(); Util::Text::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)"), }); } } } // --- 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, bool showSectionHeader) { // Weather Selection Section (only show interactive elements in inline mode) static bool weatherControlsExpanded = true; - Util::DrawSectionHeader("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("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 @@ -654,13 +673,13 @@ void WeatherEditor::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) { @@ -679,9 +698,9 @@ void WeatherEditor::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)); @@ -697,26 +716,26 @@ void WeatherEditor::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); - logger::info("[WeatherEditor] Reset weather to default"); + logger::info("[CSEditor] Reset weather to default"); } 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(); @@ -733,7 +752,7 @@ void WeatherEditor::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 @@ -746,11 +765,11 @@ void WeatherEditor::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) { @@ -787,15 +806,15 @@ 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; } 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(); } @@ -808,12 +827,14 @@ void WeatherEditor::RenderWeatherControls(RE::Sky* sky) } } -void WeatherEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) +void CSEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements, bool showSectionHeader) { ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing(); - Util::DrawSectionHeader("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) { @@ -824,10 +845,10 @@ void WeatherEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInter 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("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(); @@ -844,7 +865,7 @@ void WeatherEditor::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInter } } -void WeatherEditor::RenderCoreWeatherDetails(bool showInteractiveElements) +void CSEditor::RenderCoreWeatherDetails(bool showInteractiveElements, bool showSectionHeaders) { const auto showError = [](const char* msg) { auto menu = Menu::GetSingleton(); @@ -855,19 +876,19 @@ void WeatherEditor::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("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")); } } -void WeatherEditor::LoadAllWeathers() +void CSEditor::LoadAllWeathers() { if (s_weathersLoaded) return; @@ -891,7 +912,7 @@ void WeatherEditor::LoadAllWeathers() } } -void WeatherEditor::UpdateFilteredWeathers() +void CSEditor::UpdateFilteredWeathers() { s_filteredWeathers.clear(); for (auto weather : s_allWeathers) { @@ -930,7 +951,7 @@ void WeatherEditor::UpdateFilteredWeathers() } } -int WeatherEditor::FindWeatherIndex(RE::TESWeather* targetWeather) +int CSEditor::FindWeatherIndex(RE::TESWeather* targetWeather) { if (!targetWeather) return -1; @@ -942,13 +963,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; } @@ -961,17 +982,23 @@ void WeatherEditor::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("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) { - // 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(); @@ -979,7 +1006,7 @@ void WeatherEditor::RenderFeatureWeatherAnalysis() } } -std::vector WeatherEditor::GetWeatherFlagNames(RE::TESWeather* weather) +std::vector CSEditor::GetWeatherFlagNames(RE::TESWeather* weather) { std::vector flagNames; if (!weather) { @@ -996,15 +1023,13 @@ std::vector WeatherEditor::GetWeatherFlagNames(RE::TESWeather* weat 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") { @@ -1025,13 +1050,13 @@ std::vector WeatherEditor::GetWeatherFlagNames(RE::TESWeather* weat 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; } -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()); @@ -1084,7 +1109,11 @@ bool WeatherEditor::RenderMultiColorWeatherName(RE::TESWeather* weather, const 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(); } @@ -1093,7 +1122,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(); @@ -1116,7 +1145,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) @@ -1139,7 +1168,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"; @@ -1155,7 +1184,9 @@ std::string WeatherEditor::GetDisplayName(const RE::TESWeather* weather) return std::to_string(weather->GetFormID()); } -void WeatherEditor::DrawOverlay() +#undef I18N_KEY_PREFIX + +void CSEditor::DrawOverlay() { auto player = RE::PlayerCharacter::GetSingleton(); if (!player || !player->parentCell) @@ -1169,12 +1200,12 @@ void WeatherEditor::DrawOverlay() WeatherDetailsWindow.Enabled = true; } bool* p_open = &WeatherDetailsWindow.Enabled; - RenderWeatherDetailsWindow(p_open); + RenderWeatherDetailsWindow(p_open, false); } 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 76% rename from src/Features/WeatherEditor.h rename to src/Features/CSEditor.h index a09f1b25fe..554731c393 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/CSEditor.h @@ -1,22 +1,24 @@ #pragma once #include "Buffer.h" +#include "I18n/I18n.h" #include "Menu.h" #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 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; } virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } @@ -24,17 +26,15 @@ struct WeatherEditor : OverlayFeature virtual inline std::pair> GetFeatureSummary() override { - return { - "Development tool for editing weather, testing weather transitions, and managing weather-related feature settings.", - { "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; @@ -50,7 +50,7 @@ struct WeatherEditor : 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 WeatherEditor : 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 WeatherEditor : 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 WeatherEditor : OverlayFeature static ImVec4 GetWeatherFlagColorByName(const std::string& flagName); private: + void DrawShowInOverlayToggle(); void DrawTimeControls(); void DrawWeatherStatusPanel(); void DrawWeatherPickerSection(); @@ -158,7 +159,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/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 38707fd052..f18f961b2d 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -3,11 +3,14 @@ #include #include +#include "I18n/I18n.h" #include "ShaderCache.h" #include "State.h" #include "Utils/D3D.h" #include "Utils/UI.h" +#define I18N_KEY_PREFIX "feature.dynamic_cubemaps." + constexpr auto MIPLEVELS = 8; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( @@ -27,8 +30,8 @@ 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"); } @@ -37,13 +40,13 @@ void DynamicCubemaps::DrawSettings() 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; @@ -349,7 +352,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; @@ -390,7 +395,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; @@ -433,6 +440,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); @@ -444,6 +452,7 @@ void DynamicCubemaps::Irradiance(bool a_reflections) context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); context->Dispatch(numGroups, numGroups, 6); } + globals::profiler->EndPass(); } ID3D11ShaderResourceView* nullSRV = { nullptr }; @@ -478,6 +487,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); @@ -496,6 +506,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; @@ -814,3 +825,4 @@ void DynamicCubemaps::Reset() fakeReflections = true; } } +#undef I18N_KEY_PREFIX diff --git a/src/Features/DynamicCubemaps.h b/src/Features/DynamicCubemaps.h index 489f3abb4a..1f09741e7a 100644 --- a/src/Features/DynamicCubemaps.h +++ b/src/Features/DynamicCubemaps.h @@ -133,20 +133,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..4eb4c1cb9d 100644 --- a/src/Features/ExponentialHeightFog.cpp +++ b/src/Features/ExponentialHeightFog.cpp @@ -1,7 +1,19 @@ #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." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ExponentialHeightFog::Settings, enabled, @@ -18,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() { @@ -37,33 +84,502 @@ 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(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")); } - 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"); + 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() @@ -137,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( @@ -166,4 +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 2d2df0314d..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: @@ -8,51 +10,111 @@ 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; }; 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/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 45db9a0959..d23ad705ce 100644 --- a/src/Features/GrassCollision.cpp +++ b/src/Features/GrassCollision.cpp @@ -1,9 +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; @@ -24,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(); } } @@ -396,7 +400,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); @@ -407,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 9d2c696912..8b5286b30f 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 Open 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 Open 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 126b0a9907..323aceee28 100644 --- a/src/Features/HDRDisplay.cpp +++ b/src/Features/HDRDisplay.cpp @@ -4,16 +4,20 @@ #include "Buffer.h" #include "Globals.h" +#include "I18n/I18n.h" #include "LinearLighting.h" #include "Menu.h" #include "ShaderCache.h" #include "State.h" #include "Upscaling.h" #include "Util.h" +#include #include #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 +298,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 +338,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 +360,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 +380,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 +394,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 +406,35 @@ 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))) { + 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; @@ -432,7 +447,7 @@ void HDRDisplay::DrawSettings() ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(150, 0))) { + if (ImGui::Button(cancelLabel, ImVec2(buttonWidth, 0))) { { std::lock_guard lock(settingsMutex); settings.enableHDR = false; @@ -454,7 +469,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 +501,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 +513,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 +529,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 +549,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); @@ -1244,7 +1261,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 +1517,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/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 dfeafd7ded..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; @@ -226,7 +236,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 +254,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/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.cpp b/src/Features/InverseSquareLighting.cpp index bdff81bd57..61fc903c41 100644 --- a/src/Features/InverseSquareLighting.cpp +++ b/src/Features/InverseSquareLighting.cpp @@ -1,19 +1,10 @@ #include "InverseSquareLighting.h" +#include "CSEditor/EditorWindow.h" #include "Features/InverseSquareLighting/Common.h" #include "Features/InverseSquareLighting/RadiusMath.h" #include "LightLimitFix.h" #include -void InverseSquareLighting::DrawSettings() -{ - editor.DrawSettings(); -} - -void InverseSquareLighting::EarlyPrepass() -{ - editor.GatherLights(); -} - void InverseSquareLighting::PostPostLoad() { stl::detour_thunk(REL::RelocationID(17208, 17610)); @@ -56,13 +47,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 81946aa17c..465fc36b28 100644 --- a/src/Features/InverseSquareLighting.h +++ b/src/Features/InverseSquareLighting.h @@ -1,11 +1,12 @@ #pragma once -#include "Features/InverseSquareLighting/LightEditor.h" +#include "CSEditor/LightEditor.h" #include "LightLimitFix.h" 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"; } @@ -15,23 +16,16 @@ 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", - "Built in Light Editor for mod authors to preview lighting changes in real-time" } - }; + 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; }; - virtual void DrawSettings() override; - - virtual void EarlyPrepass() override; - virtual bool SupportsVR() override { return true; } virtual void PostPostLoad() override; 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 13a2af0d1d..78013aa874 100644 --- a/src/Features/LightLimitFix.cpp +++ b/src/Features/LightLimitFix.cpp @@ -1181,7 +1181,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); @@ -1208,7 +1210,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/LightLimitFix.h b/src/Features/LightLimitFix.h index b78f1e0186..de52e2f058 100644 --- a/src/Features/LightLimitFix.h +++ b/src/Features/LightLimitFix.h @@ -19,6 +19,7 @@ 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; } 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 0c863f93bf..8cabbbc397 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -23,12 +23,17 @@ #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" #include "Utils/FileSystem.h" #include "Utils/Format.h" #include "Utils/Game.h" #include "Utils/UI.h" + +#define I18N_KEY_PREFIX "feature.perf_overlay." + #include #include @@ -103,6 +108,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( ShowInOverlay, ShowDrawCalls, ShowVRAM, + ShowCSPasses, ShowFPS, ShowPreFGFrameTimeGraph, ShowPostFGFrameTimeGraph, @@ -130,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()); @@ -165,52 +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(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(); @@ -354,7 +344,7 @@ void PerformanceOverlay::DrawOverlay() } // Create the window - ImGui::Begin("Performance Overlay", NULL, windowFlags); + Util::BeginWithRoundedClose(T(TKEY("overlay_title"), "Performance Overlay"), nullptr, windowFlags); // Remember window position for next frame if (ImGui::IsWindowAppearing()) { @@ -373,19 +363,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 @@ -409,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 @@ -427,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); } @@ -481,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."); } @@ -510,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); @@ -529,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")); } } @@ -1317,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; } } @@ -1568,6 +1571,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 +1588,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 +1610,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 }; @@ -2014,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 d29fc6bebd..d09ba40818 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 @@ -120,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; @@ -267,6 +279,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/RenderDoc.cpp b/src/Features/RenderDoc.cpp index d02dfb21ae..19af1ac3d1 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 @@ -175,9 +179,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; @@ -194,16 +198,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()) { @@ -230,16 +234,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(); @@ -249,11 +253,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(); @@ -270,35 +274,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(); @@ -308,13 +312,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(); } @@ -323,14 +327,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 @@ -444,8 +448,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")); } } } @@ -960,3 +964,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 e5f8da05fb..6178c62757 100644 --- a/src/Features/RenderDoc.h +++ b/src/Features/RenderDoc.h @@ -51,13 +51,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 99ce602a23..84d837f2e1 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.screen_space_gi." + 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) @@ -779,7 +782,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 +814,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 +835,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 +860,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 +889,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 +917,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; @@ -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; @@ -977,3 +994,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 acce28a0cd..133eb43fa7 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -11,6 +11,7 @@ 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; } diff --git a/src/Features/ScreenSpaceShadows.cpp b/src/Features/ScreenSpaceShadows.cpp index e4418db016..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(); @@ -195,6 +198,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 +249,8 @@ void ScreenSpaceShadows::DrawShadows() if (globals::state->frameAnnotations) { globals::state->EndPerfEvent(); } + + globals::profiler->EndPass(); }; float InvTexSizeX = 1.0f / (float)viewportSize[0]; @@ -300,6 +308,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 +348,8 @@ void ScreenSpaceShadows::DrawStereoSync() context->CSSetConstantBuffers(1, 1, &cbPtr); context->CSSetShader(nullptr, nullptr, 0); + globals::profiler->EndPass(); + if (globals::state->frameAnnotations) globals::state->EndPerfEvent(); } @@ -437,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 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 d472084207..6163275cda 100644 --- a/src/Features/ScreenshotFeature.h +++ b/src/Features/ScreenshotFeature.h @@ -14,10 +14,12 @@ 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; } - 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; @@ -27,10 +29,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 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 +52,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/Features/Skin.cpp b/src/Features/Skin.cpp new file mode 100644 index 0000000000..3b32da2046 --- /dev/null +++ b/src/Features/Skin.cpp @@ -0,0 +1,626 @@ +#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" +#include "I18n/I18n.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(T("feature.skin.enable_advanced_skin", "Enable Advanced Skin"), &settings.EnableSkin); + + ImGui::Text("%s", T("feature.skin.advanced_skin_shader_using_dual_specular_lobes", "Advanced Skin Shader using dual specular lobes.")); + + ImGui::Spacing(); + ImGui::SliderFloat(T("feature.skin.primary_roughness", "Primary Roughness"), &settings.SkinMainRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.controls_microscopic_roughness_of_stratum_corneum_layer", "Controls microscopic roughness of stratum corneum layer")); + } + + ImGui::SliderFloat(T("feature.skin.secondary_roughness", "Secondary Roughness"), &settings.SkinSecondRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + 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(T("feature.skin.specular_texture_multiplier", "Specular Texture Multiplier"), &settings.SkinSpecularTexMultiplier, 0.0f, 10.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + 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(T("feature.skin.secondary_specular_strength", "Secondary Specular Strength"), &settings.SecondarySpecularStrength, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.intensity_of_secondary_specular_highlights", "Intensity of secondary specular highlights")); + } + + ImGui::SliderFloat(T("feature.skin.fresnel_f0", "Fresnel F0"), &settings.F0, 0.0f, 0.1f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.fresnel_reflectance", "Fresnel reflectance")); + } + + 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("%s", T("feature.skin.multiplier_for_the_base_color_texture", "Multiplier for the base color texture")); + } + + ImGui::Spacing(); + ImGui::Text("%s", T("feature.skin.options_for_additional_roughness_and_specular_maps", "Options for additional roughness and specular maps.")); + + 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(T("feature.skin.extra_edge_roughness", "Extra Edge Roughness"), &settings.ExtraEdgeRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + 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(T("feature.skin.fuzz_strength", "Fuzz Strength"), &settings.FuzzStrength, 0.0f, 2.0f, "%.2f"); + + ImGui::SliderFloat(T("feature.skin.fuzz_roughness", "Fuzz Roughness"), &settings.FuzzRoughness, 0.1f, 1.0f, "%.2f"); + + ImGui::SliderFloat(T("feature.skin.fuzz_f0", "Fuzz F0"), &settings.FuzzF0, 0.0f, 0.5f, "%.4f"); + + ImGui::Spacing(); + + ImGui::Checkbox(T("feature.skin.enable_sss_transmission", "Enable SSS Transmission"), &settings.UseSSS); + + ImGui::SliderFloat(T("feature.skin.translucency", "Translucency"), &settings.Translucency, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.translucency_of_the_sss_transmittance_effect", "Translucency of the SSS Transmittance effect")); + } + + ImGui::SliderFloat(T("feature.skin.sss_width", "SSS Width"), &settings.sssWidth, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.width_of_the_sss_transmittance_effect", "Width of the SSS Transmittance effect")); + } + + ImGui::Spacing(); + + 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("%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(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("%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("%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(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(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(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(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(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("%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(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("%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(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("%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(T("feature.skin.wetness_normal_scale", "Wetness Normal Scale"), &settings.WetParams.w, 0.0f, 20.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + 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(T("feature.skin.enable_skin_detail", "Enable Skin Detail"), &settings.EnableSkinDetail); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.enable_skin_detail_texture", "Enable skin detail texture")); + } + + ImGui::SliderFloat(T("feature.skin.skin_detail_strength", "Skin Detail Strength"), &settings.SkinDetailStrength, -2.0f, 2.0f); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", T("feature.skin.strength_of_skin_detail_texture", "Strength of skin detail texture")); + } + + 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("%s", T("feature.skin.the_more_tiling_the_more_detailed_the_skin", "The more tiling, the more detailed the skin will be")); + } + + 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("%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(T("feature.skin.reload_skin_detail_texture", "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..9e21044810 --- /dev/null +++ b/src/Features/Skin.h @@ -0,0 +1,148 @@ +#pragma once + +#include "I18n/I18n.h" + +struct Skin : Feature +{ + static Skin* GetSingleton() + { + static Skin singleton; + return &singleton; + } + + 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 FeatureCategories::kCharacters; } + virtual std::pair> GetFeatureSummary() override + { + return { + 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 + { + 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/SkySync.cpp b/src/Features/SkySync.cpp index 76d7b149d8..81063d16f6 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, @@ -7,69 +10,120 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( MoonLightSource, SunPath, CustomAngle, - SunriseBeginOffset, - SunriseEndOffset, - SunsetBeginOffset, - SunsetEndOffset, - MinShadowElevation) + MinShadowElevation, + ShadowTransitionDuration, + DimSunlightUnderHorizon, + NewMoonIntensity, + CrescentMoonIntensity, + FullMoonIntensity) 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 (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::TextUnformatted("Offset for when the sun starts rising."); - } - ImGui::SliderFloat("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::SliderFloat("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::SliderFloat("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::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(); } } @@ -80,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(); } @@ -110,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"); } @@ -137,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); @@ -145,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(); @@ -166,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() { @@ -217,73 +303,110 @@ 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) +// --- 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; 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); + dir = GetApparentDirection(dir, GetPlayerAltitude()); // fork: altitude correction - directions[static_cast(Caster::Sun)] = apparentDir; + SetSunPosition(sun, dir, dist); - SetSunBaseVisibility(sun, isDayTime ? 1.0f : 0.0f); + dirs[static_cast(Caster::Sun)] = dir; - 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 apparentDir = GetApparentDirection(dir, altitude); - SetMoonDirection(moon, apparentDir); + auto dir = moon->root->local.rotate.GetVectorY(); - // 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; + dir = GetApparentDirection(dir, GetPlayerAltitude()); // fork: altitude correction - if (isDayTime) - return; + dirs[idx] = dir; + + 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) @@ -314,26 +437,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; @@ -342,115 +445,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)]; - - if (fadePhase == Phase::None) { - SetLighting(sun, dir, intensity); - return; + 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 (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) @@ -471,92 +558,4 @@ 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 c1c1eed86c..52c7dd73d1 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: @@ -8,35 +10,35 @@ 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 { 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; @@ -47,8 +49,11 @@ 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; } + void OnSkyUpdateColors(RE::Sky* sky); + virtual void PostPostLoad() override; virtual void DataLoaded() override; @@ -58,18 +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 +97,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 +137,24 @@ 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); + // --- 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(); }; diff --git a/src/Features/Skylighting.cpp b/src/Features/Skylighting.cpp index 9f7f4ad608..91bdbdbc42 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,21 +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() @@ -227,7 +230,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 +518,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 +593,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; @@ -627,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 cfa1179070..3333e82a07 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) @@ -12,6 +15,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( EnableCharacterLighting, CharacterLightingStrength, SSMode, + ScatterMode, BaseProfile, HumanProfile, BurleySamples, @@ -20,75 +24,94 @@ 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.")); } 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"); + 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"); 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(); } @@ -215,6 +238,8 @@ 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; @@ -228,6 +253,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]; @@ -235,9 +261,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); @@ -247,21 +270,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"); @@ -274,19 +322,24 @@ 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 + // 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); - - context->CopyResource(main.texture, blurHorizontalTemp->resource.get()); + globals::profiler->EndPass(); } } } @@ -294,6 +347,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); @@ -329,6 +385,10 @@ void SubsurfaceScattering::SetupResources() blurHorizontalTemp = new Texture2D(texDesc); blurHorizontalTemp->CreateSRV(srvDesc); blurHorizontalTemp->CreateUAV(uavDesc); + + diffuseNoAlbedoTex = new Texture2D(texDesc); + diffuseNoAlbedoTex->CreateSRV(srvDesc); + diffuseNoAlbedoTex->CreateUAV(uavDesc); } } @@ -359,6 +419,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) @@ -368,6 +429,10 @@ void SubsurfaceScattering::SaveSettings(json& o_json) void SubsurfaceScattering::ClearShaderCache() { + if (prepassSS) { + prepassSS->Release(); + prepassSS = nullptr; + } if (horizontalSSBlur) { horizontalSSBlur->Release(); horizontalSSBlur = nullptr; @@ -382,6 +447,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) { @@ -449,3 +523,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..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,29 +68,29 @@ struct SubsurfaceScattering : Feature bool validMaterials = false; Texture2D* blurHorizontalTemp = nullptr; + Texture2D* diffuseNoAlbedoTex = nullptr; + ID3D11ComputeShader* prepassSS = nullptr; ID3D11ComputeShader* horizontalSSBlur = nullptr; ID3D11ComputeShader* verticalSSBlur = nullptr; ID3D11ComputeShader* burleySS = nullptr; 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; }; @@ -101,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(); diff --git a/src/Features/TerrainBlending.cpp b/src/Features/TerrainBlending.cpp index 5adb6a5905..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.")); } } @@ -806,7 +809,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 }; @@ -1056,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.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..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 { @@ -43,7 +42,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/Features/TerrainShadows.cpp b/src/Features/TerrainShadows.cpp index b695d85e5c..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; } @@ -413,7 +417,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/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..0de2594480 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 @@ -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/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; } diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 7defec8cc3..844ff4ef39 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" @@ -21,6 +22,8 @@ #include #include +#define I18N_KEY_PREFIX "feature.upscaling." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Upscaling::Settings, upscaleMethod, @@ -192,7 +195,10 @@ void Upscaling::DrawSettings() const bool openCompositeBlocksUpscaling = openCompositeBlocker.active; // 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); @@ -281,8 +287,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; @@ -323,12 +341,18 @@ void Upscaling::DrawSettings() } 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."); @@ -443,57 +467,57 @@ void Upscaling::DrawSettings() } } - 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) @@ -505,10 +529,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) @@ -545,7 +569,7 @@ void Upscaling::DrawSettings() // streamlineLogLevel is sanitized in LoadSettings (runs on every load, // not gated on this node being expanded), so the stored value is in range. 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); } Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::streamlineLogLevel, @@ -614,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"); @@ -1773,7 +1859,9 @@ void Upscaling::CopySharedD3D12Resources() context->PSSetShader(copyDepthToSharedBufferPS.get(), nullptr, 0); + globals::profiler->BeginPass("Upscaling::CopyDepthD3D12"); context->Draw(3, 0); + globals::profiler->EndPass(); } // Clean up @@ -2082,6 +2170,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"); @@ -2141,9 +2230,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"); @@ -2204,6 +2295,7 @@ void Upscaling::Upscale() } state->EndPerfEvent(); + globals::profiler->EndPass(); } } @@ -2390,6 +2482,7 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(2, rtvs, depth.views[0]); context->PSSetShader(depthUpscalePS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::DepthUpscale"); context->Draw(3, 0); } else { TracyD3D11Zone(globals::state->tracyCtx, "Upscaling - Full Resolution Underwater Mask Depth Copy"); @@ -2418,6 +2511,7 @@ void Upscaling::UpscaleDepth() context->OMSetRenderTargets(ARRAYSIZE(rtvs), rtvs, nullptr); context->PSSetShader(underwaterMaskPS, nullptr, 0); + globals::profiler->BeginPass("Upscaling::UnderwaterMaskUpscale"); context->Draw(3, 0); } diff --git a/src/Features/Upscaling.h b/src/Features/Upscaling.h index 27fdcaf3ca..bbc2835b7e 100644 --- a/src/Features/Upscaling.h +++ b/src/Features/Upscaling.h @@ -27,6 +27,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; } @@ -35,14 +36,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/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/Upscaling/Streamline.cpp b/src/Features/Upscaling/Streamline.cpp index 7f47aa5511..5c9f8b19e5 100644 --- a/src/Features/Upscaling/Streamline.cpp +++ b/src/Features/Upscaling/Streamline.cpp @@ -465,6 +465,9 @@ 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); + 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; 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/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index b9dc846205..4f3c4b37b6 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -1,10 +1,13 @@ #include "VolumetricLighting.h" +#include "I18n/I18n.h" #include "InteriorSun.h" #include "ShaderCache.h" #include "State.h" #include "Utils/UI.h" +#define I18N_KEY_PREFIX "feature.volumetric_lighting." + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( VolumetricLighting::TextureSize, Width, @@ -33,7 +36,7 @@ void VolumetricLighting::DrawSettings() 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 (globals::game::isVR) Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::InteriorEnabled, @@ -46,8 +49,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(); } @@ -56,19 +65,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(); diff --git a/src/Features/VolumetricLighting.h b/src/Features/VolumetricLighting.h index 4c9c7bbc15..9c058f2c8c 100644 --- a/src/Features/VolumetricLighting.h +++ b/src/Features/VolumetricLighting.h @@ -40,21 +40,19 @@ struct VolumetricLighting : Feature size_t GetSettingsBlobSize() const override { return sizeof(settings); } 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.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/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 dd077abf04..f634439a3e 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -1,6 +1,9 @@ #include "WetnessEffects.h" +#include "CSEditor.h" +#include "I18n/I18n.h" #include "Menu.h" -#include "WeatherEditor.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,145 @@ 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 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) { + 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 +394,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 +402,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 +423,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 +455,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 +567,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(); @@ -477,37 +620,37 @@ 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(T(TKEY("open_weather_picker"), "Open Weather Picker"))) { // 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("%s", T(TKEY("open_weather_picker_tooltip"), "Open the Weather Picker in CS Utility")); } } - 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(); } @@ -864,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)]; @@ -900,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; @@ -913,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(); @@ -980,4 +1121,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/Globals.cpp b/src/Globals.cpp index 222528d940..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" @@ -22,6 +23,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" @@ -35,7 +37,6 @@ #include "Features/VolumetricLighting.h" #include "Features/VolumetricShadows.h" #include "Features/WaterEffects.h" -#include "Features/WeatherEditor.h" #include "Features/WetnessEffects.h" #include "Menu.h" #include "ShaderCache.h" @@ -88,9 +89,10 @@ namespace globals RenderDoc renderDoc{}; RemoteControl remoteControl{}; ScreenshotFeature screenshotFeature{}; - WeatherEditor weatherEditor{}; + CSEditor csEditor{}; ExponentialHeightFog exponentialHeightFog{}; TruePBR truePBR{}; + Skin skin{}; namespace llf { @@ -164,6 +166,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 5b5cdf1c5c..bce0ab0b48 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -32,10 +32,12 @@ struct PerformanceOverlay; struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; -struct WeatherEditor; +class Profiler; +struct CSEditor; struct ExponentialHeightFog; struct HDRDisplay; struct ScreenshotFeature; +struct Skin; class State; class Deferred; @@ -95,9 +97,10 @@ namespace globals extern RenderDoc renderDoc; extern RemoteControl remoteControl; extern ScreenshotFeature screenshotFeature; - extern WeatherEditor weatherEditor; + extern CSEditor csEditor; extern ExponentialHeightFog exponentialHeightFog; extern TruePBR truePBR; + extern Skin skin; namespace llf { @@ -128,21 +131,21 @@ namespace globals struct FrameBufferVR { // 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 + Matrix CameraView[2]; + Matrix CameraProj[2]; + Matrix CameraViewProj[2]; + Matrix CameraViewProjUnjittered[2]; + Matrix CameraPreviousViewProjUnjittered[2]; + Matrix CameraProjUnjittered[2]; + Matrix CameraProjUnjitteredInverse[2]; + Matrix CameraViewInverse[2]; + Matrix CameraViewProjInverse[2]; + Matrix CameraProjInverse[2]; + float4 CameraPosAdjust[2]; + float4 CameraPreviousPosAdjust[2]; + float4 FrameParams; + float4 DynamicResolutionParams1; + float4 DynamicResolutionParams2; }; union FrameBufferCache @@ -271,6 +274,7 @@ namespace globals extern Deferred* deferred; extern Menu* menu; extern SIE::ShaderCache* shaderCache; + extern Profiler* profiler; void OnInit(); void ReInit(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index b2e487c742..cbf0021d1a 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -13,13 +13,14 @@ #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/LightLimitFix.h" +#include "Features/ScreenshotFeature.h" +#include "Features/Skin.h" +#include "Features/SkySync.h" #include "Features/Upscaling.h" #include "Features/Upscaling/FoveatedRender/Bridge.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" -#include "ShaderTools/BSShaderHooks.h" - std::unordered_map, size_t>> ShaderBytecodeMap; void RegisterShaderBytecode(void* Shader, const void* Bytecode, size_t BytecodeLength) @@ -267,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; @@ -558,8 +562,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; }; @@ -596,8 +601,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; }; @@ -606,8 +612,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; }; @@ -616,8 +623,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; }; @@ -644,6 +652,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) @@ -947,6 +977,12 @@ namespace Hooks func(a_pass, a_technique, a_alphaTest, a_renderFlags); } + 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. * @@ -995,6 +1031,9 @@ 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, 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)); stl::write_thunk_call(REL::RelocationID(100458, 107175).address() + REL::Relocate(0xA59, 0xA59, 0xD13)); @@ -1018,6 +1057,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 fb23eb1049..e26cb61b35 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -31,6 +31,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/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 cbf202da14..a14f02dbd9 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -24,8 +24,10 @@ #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/CursorLoader.h" #include "Menu/FeatureListRenderer.h" #include "Menu/Fonts.h" #include "Menu/HomePageRenderer.h" @@ -39,13 +41,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/WeatherEditor.h" -#include "WeatherEditor/EditorWindow.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -135,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, @@ -149,6 +164,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( CenterHeader, TooltipHoverDelay, BackgroundBlurEnabled, + UseCustomCursor, + Cursor, ScrollbarOpacity, Palette, StatusPalette, @@ -163,7 +180,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( OverlayToggleKey, ShaderBlockPrevKey, ShaderBlockNextKey, - WeatherEditorToggleKey, + CSEditorToggleKey, EnableShaderBlocking, FirstTimeSetupCompleted, SkipClearCacheConfirmation, @@ -177,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) @@ -305,6 +385,8 @@ Menu::~Menu() uiIcons.playMode.Release(); uiIcons.search.Release(); + Util::CursorLoader::Shutdown(); + // Clean up blur resources BackgroundBlur::Cleanup(); @@ -341,7 +423,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 @@ -362,7 +444,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 @@ -371,6 +453,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)]; @@ -427,7 +512,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); } @@ -438,7 +523,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)) { @@ -467,6 +556,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() @@ -500,8 +590,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); @@ -520,6 +614,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); @@ -627,6 +722,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"); @@ -685,7 +782,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(); @@ -760,7 +857,7 @@ void Menu::DrawGeneralSettings() .settingOverlayToggleKey = settingOverlayToggleKey, .settingShaderBlockPrevKey = settingShaderBlockPrevKey, .settingShaderBlockNextKey = settingShaderBlockNextKey, - .settingWeatherEditorToggleKey = settingWeatherEditorToggleKey, + .settingCSEditorToggleKey = settingCSEditorToggleKey, .settingScreenshotKey = settingScreenshotKey }; @@ -786,14 +883,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) { @@ -815,11 +913,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()); } /** @@ -860,6 +968,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(); }, @@ -886,7 +1005,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; @@ -1000,14 +1119,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; @@ -1067,7 +1186,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; @@ -1078,7 +1197,7 @@ void Menu::ProcessInputEventQueue() // Locked or PlayMode → fully exit preview ew->ExitPreviewMode(); } else { - WeatherEditor::ToggleEditorWindow(); + CSEditor::ToggleEditorWindow(); } } }, { settings.ScreenshotKey, []() { @@ -1114,7 +1233,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), @@ -1145,7 +1264,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) @@ -1225,23 +1344,23 @@ 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; - weather.RenderWeatherDetailsWindow(p_open); + auto& weather = globals::features::csEditor; + bool* p_open = &globals::features::csEditor.WeatherDetailsWindow.Enabled; + weather.RenderWeatherDetailsWindow(p_open, !weather.WeatherDetailsWindow.ShowInOverlay); } /** diff --git a/src/Menu.h b/src/Menu.h index 1b9bf6578c..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 settingWeatherEditorToggleKey = false; // Weather 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 @@ -206,6 +206,7 @@ class Menu // Deferred reload systems (public for SettingsTabRenderer access) bool pendingFontReload = false; bool pendingIconReload = false; + bool pendingCursorReload = false; private: // Off-thread visibility requests, consumed on the render thread in @@ -252,11 +253,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) // Category icons UIIcon characters; @@ -312,6 +313,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 { @@ -444,23 +460,26 @@ 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) }; 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 WeatherEditorToggleKey = { InputCombo::Keyboard(VK_SHIFT), InputCombo::Keyboard(VK_END) }; // Weather 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 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 ThemeSettings Theme; std::string SelectedThemePreset = ""; // Currently selected theme preset (empty = custom/user theme) }; @@ -554,4 +573,4 @@ class Menu void ProcessInputEventQueue(); bool IsCapturingHotkeyInput() const; winrt::com_ptr dxgiAdapter3; -}; \ No newline at end of file +}; diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 0738816f32..d5ea7de491 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" @@ -103,7 +104,7 @@ void AdvancedSettingsRenderer::RenderShaderCompileFlags() // 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() && @@ -113,7 +114,7 @@ void AdvancedSettingsRenderer::RenderShaderCompileFlags() 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.")); } // Half-precision (partial precision) shader compile flag @@ -177,17 +178,17 @@ void AdvancedSettingsRenderer::RenderShaderThreading() ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, maxThreads); 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, maxThreads); 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.")); } } @@ -210,19 +211,19 @@ void AdvancedSettingsRenderer::RenderShaderCacheControls() // 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.")); } } @@ -244,28 +245,28 @@ void AdvancedSettingsRenderer::RenderShaderReplacementTable() 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(); @@ -488,25 +489,26 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() 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) { @@ -524,7 +526,7 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() 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")); } } @@ -545,11 +547,11 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() 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) { @@ -557,32 +559,32 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() // 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; } } @@ -597,10 +599,10 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() ImGui::Spacing(); Util::DrawSectionHeader("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 @@ -628,20 +630,20 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() // 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; } } }; @@ -674,14 +676,16 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() 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) @@ -737,7 +741,7 @@ void AdvancedSettingsRenderer::RenderShaderBlockingPanel() // 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 @@ -788,7 +792,7 @@ void AdvancedSettingsRenderer::RenderTestingSection() 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/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/CursorLoader.cpp b/src/Menu/CursorLoader.cpp new file mode 100644 index 0000000000..dae9b42716 --- /dev/null +++ b/src/Menu/CursorLoader.cpp @@ -0,0 +1,206 @@ +#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/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index c8f66ffef0..0823a34433 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -12,21 +12,43 @@ #include "Feature.h" #include "FeatureConstraints.h" #include "FeatureIssues.h" +#include "Features/CSEditor.h" #include "Fonts.h" #include "Globals.h" +#include "I18n/I18n.h" #include "Menu.h" #include "Menu/HomePageRenderer.h" +#include "Menu/ProfilingRenderer.h" #include "Menu/ThemeManager.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" #include "State.h" #include "Util.h" +#include "Utils/UI.h" #include "WeatherVariableRegistry.h" namespace { // Core built-in menu names that always appear first in the menu list - constexpr std::array CORE_MENU_NAMES = { "Home", "General", "Advanced", "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) { @@ -109,6 +131,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 @@ -267,7 +317,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 @@ -278,9 +328,10 @@ std::vector FeatureListRenderer::BuildMenuLis } auto menuList = std::vector{ - BuiltInMenu{ "Home", []() { HomePageRenderer::RenderHomePage(); } }, - BuiltInMenu{ "General", drawGeneralSettings }, - BuiltInMenu{ "Advanced", drawAdvancedSettings } + 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. @@ -297,7 +348,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(); }); } @@ -343,12 +394,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(); } }); } @@ -411,7 +462,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) @@ -441,7 +492,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.")); } } @@ -450,7 +501,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); @@ -468,7 +519,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 @@ -486,7 +537,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 @@ -529,7 +581,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(); @@ -547,7 +599,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(); @@ -564,7 +616,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) @@ -609,7 +661,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; @@ -634,7 +686,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(); @@ -666,11 +718,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) @@ -691,13 +744,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.")); } } } @@ -711,21 +768,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(); } @@ -741,9 +800,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(); } @@ -755,6 +815,12 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImVec2 cursorPosBefore = ImGui::GetCursorPos(); feat->DrawSettings(); + + if (feat != &globals::features::csEditor && ProfilingRenderer::HasFeatureTimers(feat->GetShortName())) { + ImGui::SeparatorText(T("menu.features.profiling", "Profiling")); + ProfilingRenderer::RenderFeatureTimers(feat->GetShortName()); + } + ImVec2 cursorPosAfter = ImGui::GetCursorPos(); if (sceneControlled) @@ -806,23 +872,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.")); } } } @@ -831,7 +899,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()); } } @@ -872,7 +940,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")); } } @@ -882,27 +950,30 @@ 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)) { - ImGui::TextWrapped("Some of your settings have been automatically adjusted due to feature incompatibilities."); + 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(); 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; @@ -916,7 +987,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; } } @@ -928,7 +999,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()); } } @@ -947,11 +1018,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(); @@ -974,13 +1045,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(); @@ -991,7 +1064,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; @@ -1008,4 +1081,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 44ffe881e0..09a41cf4d1 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" @@ -99,9 +100,10 @@ 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); // Nexus button → the Open Shaders fork page (mod 180419). ImGui::Columns(3, nullptr, false); @@ -127,9 +129,10 @@ 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 @@ -143,32 +146,32 @@ void HomePageRenderer::RenderFAQSection() "settings and themes are compatible."); } - 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 Open Shaders compatible with ENB?")) { @@ -218,12 +221,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(); @@ -256,14 +258,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 = { @@ -287,7 +289,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()); @@ -405,7 +410,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); @@ -468,20 +473,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); } - // 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; - if (weatherKey.empty()) { - const char* warnText = "Weather 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 = "Weather 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()); } @@ -489,7 +496,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); @@ -502,7 +509,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/IconLoader.cpp b/src/Menu/IconLoader.cpp index 8c37c472e1..de355f401a 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) { @@ -155,7 +153,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++; } @@ -216,7 +214,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 { @@ -227,7 +225,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/MenuHeaderRenderer.cpp b/src/Menu/MenuHeaderRenderer.cpp index 0caec58d0a..27765e3dcd 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(); } }); @@ -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/Menu/OverlayRenderer.cpp b/src/Menu/OverlayRenderer.cpp index ed50ae1a3b..416c587e9c 100644 --- a/src/Menu/OverlayRenderer.cpp +++ b/src/Menu/OverlayRenderer.cpp @@ -10,15 +10,17 @@ #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 "Menu/CursorLoader.h" #include "ShaderCache.h" #include "State.h" #include "Util.h" -#include "WeatherEditor/EditorWindow.h" #include "Features/PerformanceOverlay.h" #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" @@ -44,7 +46,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.")); } } @@ -305,7 +307,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); @@ -382,6 +384,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 @@ -432,7 +438,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/ProfilingRenderer.cpp b/src/Menu/ProfilingRenderer.cpp new file mode 100644 index 0000000000..98ab52fe46 --- /dev/null +++ b/src/Menu/ProfilingRenderer.cpp @@ -0,0 +1,472 @@ +#include "ProfilingRenderer.h" + +#include +#include +#include +#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) +{ + 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; +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) +{ + 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::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); + 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() * kGraphHeadroomScale; + if (maxFrameTimeSec < kMainGraphMinFrameTimeSec) + maxFrameTimeSec = kMainGraphMinFrameTimeSec; + + const auto layout = GetGraphLayout(ImGui::GetContentRegionAvail().x, kMainGraphLegendWidth, kMainGraphHeight); + + gpuGraph.RenderTimings(layout.graphWidth, layout.legendWidth, layout.height, 0, maxFrameTimeSec, layout.uiScale); + + ImGui::Spacing(); +} + +void ProfilingRenderer::RenderStatistics(bool showTable, bool showModeToggle) +{ + auto& profiler = (*globals::profiler); + + bool cpuMode = (timingMode == TimingMode::CPU); + if (showModeToggle) { + RenderTimingModeToggle(); + cpuMode = (timingMode == TimingMode::CPU); + ImGui::Separator(); + } + + float currentTime = static_cast(ImGui::GetTime()); + float deltaTime = currentTime - lastFrameTime; + lastFrameTime = currentTime; + timeSinceLastUpdate += deltaTime; + + if (timeSinceLastUpdate >= kStatsRefreshSeconds) { + 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("%s", T("menu.profiling.no_timing_data_world", "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); + SetupTimingTableColumns(true); + 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(); + + RenderTimingModeToggle(); + + 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; + + const auto prefix = GetFeatureTimerPrefix(featurePrefix); + for (const auto& r : results) { + if (!IsFeatureTimerResult(r, 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("%s", T("menu.profiling.no_timing_data", "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() * kGraphHeadroomScale; + if (maxFrameTimeSec < kFeatureGraphMinFrameTimeSec) + maxFrameTimeSec = kFeatureGraphMinFrameTimeSec; + + const auto layout = GetGraphLayout(ImGui::GetContentRegionAvail().x, kFeatureGraphLegendWidth, kFeatureGraphHeight); + + 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)) { + SetupTimingTableColumns(false); + 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(); + const auto totalColor = globals::menu->GetTheme().StatusPalette.InfoColor; + ImGui::TextColored(totalColor, "%s", T("menu.profiling.total", "Total")); + ImGui::TableNextColumn(); + ImGui::TextColored(totalColor, "%.3f", totalAvg); + ImGui::TableNextColumn(); + ImGui::TextColored(totalColor, "%.3f", totalP95); + ImGui::TableNextColumn(); + ImGui::TextColored(totalColor, "%.3f", totalP99); + + 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 new file mode 100644 index 0000000000..c247e58e25 --- /dev/null +++ b/src/Menu/ProfilingRenderer.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#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); + static bool HasFeatureTimers(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 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); +}; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index d639d3ffd7..d4f8961094 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -1,14 +1,17 @@ #include "SettingsTabRenderer.h" +#include #include #include #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 "IconLoader.h" #include "Menu.h" #include "ShaderCache.h" @@ -22,134 +25,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 +199,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,51 +219,52 @@ 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!")); } 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 @@ -275,12 +289,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(); @@ -334,41 +348,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( - "Weather Editor Toggle Key:", - settings.WeatherEditorToggleKey, - state.settingWeatherEditorToggleKey, - "Change##WeatherEditorToggle"); + 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"); @@ -379,7 +394,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(); @@ -395,81 +411,138 @@ 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); - ImGui::Checkbox("Show Icon Buttons in Header", &themeSettings.ShowActionIcons); + { + 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(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 logo"); + ImGui::Text("%s", T("menu.settings.use_monochrome_cs_logo_tooltip", "Uses monochrome version of the 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 title and logo in the header title bar"); + ImGui::Text("%s", T("menu.settings.center_header_title_tooltip", "Centers the 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.")); } // Skip confirmation when clearing shader cache (UI behavior, not a shader setting). auto& menuSettings = globals::menu->GetSettings(); bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; - if (ImGui::Checkbox("Skip Clear Cache Confirmation", &skipConfirmation)) { + if (ImGui::Checkbox(T("menu.settings.skip_clear_cache_dialogue", "Skip Clear Cache Confirmation"), &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 (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("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 +551,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 +578,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 +634,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 +652,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 +671,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 +684,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 +743,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 +759,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 +781,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 +791,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 +799,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 +828,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 +838,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 +854,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 +909,7 @@ void SettingsTabRenderer::RenderThemesTab() } ImGui::SameLine(); - if (ImGui::Button("Cancel")) { + if (ImGui::Button(T("menu.settings.cancel", "Cancel"))) { showCreateThemePopup = false; ImGui::CloseCurrentPopup(); } @@ -853,15 +932,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 +950,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 +974,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 +1004,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 +1044,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 +1056,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 +1084,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 +1099,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 +1116,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 +1129,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 +1219,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(); @@ -1136,84 +1228,72 @@ 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)); - colorFilter.Draw("Filter colors", availableWidth); + 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(); // 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); @@ -1226,4 +1306,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..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& settingWeatherEditorToggleKey; // Weather 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( @@ -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/Menu/ThemeManager.cpp b/src/Menu/ThemeManager.cpp index 31da7155e5..b43784f8c6 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 @@ -120,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); @@ -360,6 +449,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/Menu/ThemeManager.h b/src/Menu/ThemeManager.h index 866810bb20..445bae36a0 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": { @@ -199,7 +207,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 +339,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/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 82342b5824..8dadef82be 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -6,19 +6,22 @@ #include "Deferred.h" #include "FeatureIssues.h" +#include "Features/CSEditor.h" #include "Features/CloudShadows.h" #include "Features/DynamicCubemaps.h" +#include "Features/ExponentialHeightFog.h" #include "Features/FoveatedCommon.h" #include "Features/HDRDisplay.h" #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" #include "Features/VR.h" #include "Features/VRStereoOptimizations.h" #include "Features/VolumetricShadows.h" -#include "Features/WeatherEditor.h" #include "Menu.h" #include "SceneSettingsManager.h" #include "SettingsOverrideManager.h" @@ -55,7 +58,8 @@ 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; auto& volumetricShadows = globals::features::volumetricShadows; @@ -64,7 +68,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(); } @@ -80,13 +84,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) { @@ -99,6 +108,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(); } } } @@ -167,6 +178,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(); @@ -211,6 +224,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 @@ -359,6 +377,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); @@ -450,6 +472,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; @@ -531,6 +554,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()) { @@ -660,9 +700,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)); } @@ -743,8 +783,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 @@ -760,6 +800,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) @@ -993,6 +1043,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; @@ -1043,8 +1121,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); diff --git a/src/State.h b/src/State.h index 487a321307..84e50a6f1b 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(); @@ -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; @@ -286,6 +292,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/TruePBR.cpp b/src/TruePBR.cpp index 34a47882dc..b4b46b99d3 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(); @@ -1529,7 +1535,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..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; } @@ -58,7 +59,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(); diff --git a/src/Utils/FileSystem.cpp b/src/Utils/FileSystem.cpp index 782dfda6dd..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"; @@ -82,6 +87,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..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" @@ -91,6 +97,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/LegitProfiler.h b/src/Utils/LegitProfiler.h new file mode 100644 index 0000000000..837fdf8d7f --- /dev/null +++ b/src/Utils/LegitProfiler.h @@ -0,0 +1,295 @@ +// 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(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(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: + 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, float uiScale) + { + Rect(drawList, graphPos, ImVec2(graphPos.x + graphSize.x, graphPos.y + graphSize.y), 0xffffffff, false); + 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 - 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]; + 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 + scaledFrameWidth, taskPos.y - taskEndHeight), task.color, true); + } + } + } + + void RenderLegend(ImDrawList* drawList, ImVec2 legendPos, ImVec2 legendSize, size_t frameIndexOffset, float maxFrameTime, float uiScale) + { + 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)); + + 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; + }; +} 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 }; + } + +} diff --git a/src/Utils/Subrect.cpp b/src/Utils/Subrect.cpp index 84d0eb61bb..07eafdef81 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 #include diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index cbb50b1c58..bd20907d3d 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1,6 +1,7 @@ #include "UI.h" -#include "../WeatherEditor/EditorWindow.h" +#include "../CSEditor/EditorWindow.h" +#include "../I18n/I18n.h" #include "D3D.h" #include "FileSystem.h" #include "Menu.h" @@ -39,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -125,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() @@ -280,21 +282,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 +310,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 +325,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 +364,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; @@ -617,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; } @@ -742,36 +831,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 +872,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 +885,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 +939,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) { @@ -1184,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; @@ -1192,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 @@ -1224,16 +1315,17 @@ 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); 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( @@ -1257,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(); @@ -1280,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); @@ -1288,13 +1381,13 @@ 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; } // 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); @@ -2115,7 +2208,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(); } @@ -2169,7 +2262,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(); } @@ -2220,7 +2313,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(); } @@ -2271,7 +2364,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(); } diff --git a/src/Utils/UI.h b/src/Utils/UI.h index a59605200f..884993c19d 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -13,12 +13,14 @@ #include "../FeatureConstraints.h" #include "../Menu/Fonts.h" +#include "../Menu/ThemeManager.h" #include "Utils/BootSnapshot.h" #include "Utils/Input.h" // Forward declarations struct ID3D11Device; struct ID3D11ShaderResourceView; +struct ImRect; struct ImVec2; class Menu; class Feature; @@ -69,11 +71,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: @@ -323,8 +329,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) @@ -501,7 +516,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 @@ -865,7 +880,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. diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index db89fb661d..bd92808fca 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() 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()