From 5e8d15e33420245667e3533c3bd3ce02a53ac2eb Mon Sep 17 00:00:00 2001 From: "Skyth (Asilkan)" <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Fri, 14 Feb 2025 01:41:18 +0300 Subject: [PATCH] Implement options menu TV static animation. (#403) --- UnleashedRecomp/CMakeLists.txt | 7 +- UnleashedRecomp/ui/options_menu.cpp | 25 +- UnleashedRecomp/ui/tv_static.cpp | 392 ++++++++++++++++++++++++++++ UnleashedRecomp/ui/tv_static.h | 8 + UnleashedRecompResources | 2 +- 5 files changed, 430 insertions(+), 4 deletions(-) create mode 100644 UnleashedRecomp/ui/tv_static.cpp create mode 100644 UnleashedRecomp/ui/tv_static.h diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 78b8260..fd372be 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -158,7 +158,8 @@ set(UNLEASHED_RECOMP_UI_CXX_SOURCES "ui/installer_wizard.cpp" "ui/message_window.cpp" "ui/options_menu.cpp" - "ui/options_menu_thumbnails.cpp" + "ui/options_menu_thumbnails.cpp" + "ui/tv_static.cpp" ) set(UNLEASHED_RECOMP_INSTALL_CXX_SOURCES @@ -552,7 +553,9 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/op BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/thumbnails/vsync_off.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/thumbnails/vsync_off.dds" ARRAY_NAME "g_vsync_off" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/thumbnails/window_size.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/thumbnails/window_size.dds" ARRAY_NAME "g_window_size" COMPRESSION_TYPE "zstd") BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/thumbnails/xbox_color_correction.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/thumbnails/xbox_color_correction.dds" ARRAY_NAME "g_xbox_color_correction" COMPRESSION_TYPE "zstd") -BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/miles_electric.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/miles_electric.dds" ARRAY_NAME "g_miles_electric" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/miles_electric.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/miles_electric.dds" ARRAY_NAME "g_miles_electric" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/options_static.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/options_static.dds" ARRAY_NAME "g_options_static" COMPRESSION_TYPE "zstd") +BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/options_menu/options_static_flash.dds" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/options_menu/options_static_flash.dds" ARRAY_NAME "g_options_static_flash" COMPRESSION_TYPE "zstd") ## Game Icon ## BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon.bmp" ARRAY_NAME "g_game_icon") diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index 1e4d985..cde2799 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -1,5 +1,6 @@ #include "options_menu.h" #include "options_menu_thumbnails.h" +#include "tv_static.h" #include #include @@ -1436,7 +1437,27 @@ static void DrawInfoPanel(ImVec2 infoMin, ImVec2 infoMax) ImVec2 thumbnailMin = { clipRectMin.x, clipRectMin.y + Scale(GRID_SIZE / 2.0f) }; ImVec2 thumbnailMax = { clipRectMax.x, thumbnailMin.y + thumbnailHeight }; - drawList->AddImage(thumbnail, thumbnailMin, thumbnailMax); + if (g_isStage) + { + drawList->AddImage(thumbnail, thumbnailMin, thumbnailMax); + } + else + { + float time = g_appearTime + CONTAINER_FULL_DURATION / 60.0; + + drawList->AddImage( + thumbnail, + thumbnailMin, + thumbnailMax, + { 0.0f, 0.0f }, + { 1.0f, 1.0f }, + IM_COL32(255, 255, 255, 255 * TVStatic::ComputeThumbnailAlpha(time))); + + TVStatic::Draw( + { (thumbnailMin.x + thumbnailMax.x) / 2.0f, (thumbnailMin.y + thumbnailMax.y) / 2.0f }, + { (thumbnailMax.x - thumbnailMin.x), (thumbnailMax.y - thumbnailMin.y) }, + time); + } if (g_inaccessibleReason) { @@ -1688,6 +1709,8 @@ void OptionsMenu::Init() LoadThumbnails(); g_upMilesElectric = LOAD_ZSTD_TEXTURE(g_miles_electric); + + TVStatic::Init(); } void OptionsMenu::Draw() diff --git a/UnleashedRecomp/ui/tv_static.cpp b/UnleashedRecomp/ui/tv_static.cpp new file mode 100644 index 0000000..4178356 --- /dev/null +++ b/UnleashedRecomp/ui/tv_static.cpp @@ -0,0 +1,392 @@ +#include "tv_static.h" +#include "imgui_utils.h" + +#include +#include +#include +#include +#include + +namespace +{ + struct FloatLinear + { + float value; + float time; + + static float Sample(const FloatLinear& a, const FloatLinear& b, float time) + { + const float t = std::clamp((time - a.time) / (b.time - a.time), 0.0f, 1.0f); + return (b.value - a.value) * t + a.value; + } + }; + + struct FloatHermite + { + float value; + float time; + float inTangent; + float outTangent; + + static float Sample(const FloatHermite& a, const FloatHermite& b, float time) + { + const float t = std::clamp((time - a.time) / (b.time - a.time), 0.0f, 1.0f); + + float valueDelta = b.value - a.value; + float frameDelta = b.time - a.time; + + float biasSquaric = t * t; + float biasCubic = biasSquaric * t; + + float valueCubic = (a.outTangent + a.inTangent) * frameDelta - valueDelta * 2.0f; + float valueSquaric = valueDelta * 3.0f - (a.inTangent * 2.0f + a.outTangent) * frameDelta; + float valueLinear = frameDelta * a.inTangent; + + return valueCubic * biasCubic + valueSquaric * biasSquaric + valueLinear * t + a.value; + } + }; +} + +template +static auto Sample(const std::array& keys, float time) +{ + T firstKey = keys[0]; + T lastKey = keys[N - 1]; + + if (time < firstKey.time) + return firstKey.value; + + if (time > lastKey.time) + return lastKey.value; + + size_t keyIndex = 0; + for (auto key : keys) + { + if (key.time >= time) + break; + + keyIndex++; + } + + if (keyIndex >= N) + return keys[N - 1].value; + + return T::Sample(keys[keyIndex - 1], keys[keyIndex], time); +} + +static std::unique_ptr g_flashTexture; +static std::unique_ptr g_noiseTexture; + +static constexpr float FRAME_OFFSET = 65.0f; +static constexpr float FRAME_SCALE = 1.0f / 60.0f; +static constexpr float FRAME_DURATION = 32.0f; + +static std::array g_flashScaleX = +{ + FloatHermite{ 0, 67 * FRAME_SCALE, 0, 0.673736f }, + FloatHermite{ 2, 70 * FRAME_SCALE, -0.003465f, -0.682543f }, + FloatHermite{ 0, 73 * FRAME_SCALE, 0, 0 }, +}; +static std::array g_flashScaleY = +{ + FloatHermite{ 0, 67 * FRAME_SCALE, 0, 0.67238f }, + FloatHermite{ 2, 70 * FRAME_SCALE, -0.001741f, -0.664096f }, + FloatHermite{ 0, 73 * FRAME_SCALE, 0, 0 }, +}; +static std::array g_flashColor = +{ + FloatLinear{ 255.0f, 67 * FRAME_SCALE }, + FloatLinear{ 160.0f, 70 * FRAME_SCALE }, + FloatLinear{ 255.0f, 73 * FRAME_SCALE }, +}; +static std::array g_flashAlpha = +{ + FloatLinear{ 255.0f, 70 * FRAME_SCALE }, + FloatLinear{ 0.0f, 73 * FRAME_SCALE }, +}; +static std::array g_noiseScale = +{ + FloatLinear{ 0.0f, 70 * FRAME_SCALE }, + FloatLinear{ 1.0f, 75 * FRAME_SCALE }, +}; +static std::array g_noiseTL_gradTL = +{ + FloatLinear{ 0.0f, 77 * FRAME_SCALE }, + FloatLinear{ (192 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 90 * FRAME_SCALE }, +}; +static std::array g_noiseTL_gradBL = +{ + FloatLinear{ 0.0f, 73 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 94 * FRAME_SCALE }, +}; +static std::array g_noiseTL_gradTR = +{ + FloatLinear{ 0.0f, 74 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 93 * FRAME_SCALE }, +}; +static std::array g_noiseTL_gradBR = +{ + FloatLinear{ 0.0f, 70 * FRAME_SCALE }, + FloatLinear{ 1.0f, 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 97 * FRAME_SCALE }, +}; +static std::array g_noiseTR_gradTL = +{ + FloatLinear{ 0.0f, 74 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 93 * FRAME_SCALE }, +}; +static std::array g_noiseTR_gradBL = +{ + FloatLinear{ 0.0f, 70 * FRAME_SCALE }, + FloatLinear{ 1.0f, 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 97 * FRAME_SCALE }, +}; +static std::array g_noiseTR_gradTR = +{ + FloatLinear{ 0.0f, 77 * FRAME_SCALE }, + FloatLinear{ (192 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 90 * FRAME_SCALE }, +}; +static std::array g_noiseTR_gradBR = +{ + FloatLinear{ 0.0f, 73 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 94 * FRAME_SCALE }, +}; +static std::array g_noiseBL_gradTL = +{ + FloatLinear{ 0.0f, 73 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 94 * FRAME_SCALE }, +}; +static std::array g_noiseBL_gradBL = +{ + FloatLinear{ 0.0f, 77 * FRAME_SCALE }, + FloatLinear{ (192 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 90 * FRAME_SCALE }, +}; +static std::array g_noiseBL_gradTR = +{ + FloatLinear{ 0.0f, 70 * FRAME_SCALE }, + FloatLinear{ 1.0f, 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 97 * FRAME_SCALE }, +}; +static std::array g_noiseBL_gradBR = +{ + FloatLinear{ 0.0f, 74 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 93 * FRAME_SCALE }, +}; +static std::array g_noiseBR_gradTL = +{ + FloatLinear{ 0.0f, 70 * FRAME_SCALE }, + FloatLinear{ 1.0f, 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 97 * FRAME_SCALE }, +}; +static std::array g_noiseBR_gradBL = +{ + FloatLinear{ 0.0f, 74 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 93 * FRAME_SCALE }, +}; +static std::array g_noiseBR_gradTR = +{ + FloatLinear{ 0.0f, 73 * FRAME_SCALE }, + FloatLinear{ (223 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 94 * FRAME_SCALE }, +}; +static std::array g_noiseBR_gradBR = +{ + FloatLinear{ 0.0f, 77 * FRAME_SCALE }, + FloatLinear{ (192 / 255.0f), 85 * FRAME_SCALE }, + FloatLinear{ 0.0f, 90 * FRAME_SCALE }, +}; +static std::array g_thumbnailAlpha = +{ + FloatLinear{ 0.0f, 85 * FRAME_SCALE }, + FloatLinear{ 1.0f, 90 * FRAME_SCALE }, +}; + +static std::pair ComputeRect(const ImVec2& center, const ImVec2& scale) +{ + ImVec2 min = { center.x - scale.x / 2.0f, center.y - scale.y / 2.0f }; + ImVec2 max = { center.x + scale.x / 2.0f, center.y + scale.y / 2.0f }; + + return std::make_pair(min, max); +} + +static void DrawFlash(const ImVec2& center, const ImVec2& resolution, float time) +{ + auto drawList = ImGui::GetBackgroundDrawList(); + + float baseScale = resolution.y / 135.0f * 100.0f; + auto [min, max] = ComputeRect(center, { Sample(g_flashScaleX, time) * baseScale, Sample(g_flashScaleY, time) * baseScale}); + + float color = Sample(g_flashColor, time); + float alpha = Sample(g_flashAlpha, time); + + drawList->AddImage(g_flashTexture.get(), min, max, { 0.0f, 0.0f }, { 1.0f, 1.0f }, IM_COL32(color, 255, color, alpha)); +} + +static void PrimRectUVColorCorners(ImDrawList* This, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, + ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left, bool reverse_order = false) +{ + ImVec2 a(p_min), c(p_max), uv_a(uv_min), uv_c(uv_max); + ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); + + if (reverse_order) + { + ImVec2 _a = a; a = b; b = _a; + ImVec2 _c = c; c = d; d = _c; + + ImVec2 _uv_a = uv_a; uv_a = uv_b; uv_b = _uv_a; + ImVec2 _uv_c = uv_c; uv_c = uv_d; uv_d = _uv_c; + } + + ImDrawIdx idx = (ImDrawIdx)This->_VtxCurrentIdx; + This->_IdxWritePtr[0] = idx; This->_IdxWritePtr[1] = (ImDrawIdx)(idx + 1); This->_IdxWritePtr[2] = (ImDrawIdx)(idx + 2); + This->_IdxWritePtr[3] = idx; This->_IdxWritePtr[4] = (ImDrawIdx)(idx + 2); This->_IdxWritePtr[5] = (ImDrawIdx)(idx + 3); + This->_VtxWritePtr[0].pos = a; This->_VtxWritePtr[0].uv = uv_a; This->_VtxWritePtr[0].col = reverse_order ? col_upr_right : col_upr_left; + This->_VtxWritePtr[1].pos = b; This->_VtxWritePtr[1].uv = uv_b; This->_VtxWritePtr[1].col = reverse_order ? col_upr_left : col_upr_right; + This->_VtxWritePtr[2].pos = c; This->_VtxWritePtr[2].uv = uv_c; This->_VtxWritePtr[2].col = reverse_order ? col_bot_left : col_bot_right; + This->_VtxWritePtr[3].pos = d; This->_VtxWritePtr[3].uv = uv_d; This->_VtxWritePtr[3].col = reverse_order ? col_bot_right : col_bot_left; + This->_VtxWritePtr += 4; + This->_VtxCurrentIdx += 4; + This->_IdxWritePtr += 6; +} + +static void AddImageGradient(ImDrawList* DrawList, ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, + ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left, bool reverse_order = false) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id && (user_texture_id != DrawList->_CmdHeader.TextureId); + if (push_texture_id) + DrawList->PushTextureID(user_texture_id); + + DrawList->PrimReserve(6, 4); + + const ImVec2 uv = DrawList->_Data->TexUvWhitePixel; + + PrimRectUVColorCorners(DrawList, p_min, p_max, + user_texture_id ? uv_min : uv, + user_texture_id ? uv_max : uv, + col_upr_left, col_upr_right, col_bot_right, col_bot_left, + reverse_order); + + if (push_texture_id) + DrawList->PopTextureID(); +} + +static void DrawStatic(const ImVec2& center, const ImVec2& resolution, float time) +{ + auto drawList = ImGui::GetBackgroundDrawList(); + + const float scaleFactor = Sample(g_noiseScale, time); + + const ImVec2 scale2D = { 1, scaleFactor }; + + auto [min, max] = ComputeRect(center, {scale2D.x * resolution.x, scale2D.y * resolution.y}); + + // Texture pixel size divided by image resolution (which must be divisible by 4) to get proper UV clipping. + ImVec2 UV_MAX = { 270.0f / 272.0f, + 135.0f / 136.0f }; + + // Corner points + + ImVec2 topCenter = { center.x, min.y }; + ImVec2 bottomCenter = { center.x, max.y }; + + ImVec2 centerLeft = { min.x, center.y }; + ImVec2 centerRight = { max.x, center.y }; + + bool flipUV = time >= (73 * FRAME_SCALE); + + const float UV_1 = flipUV ? 0.0f : 1.0f; + const float UV_0 = flipUV ? 1.0f : 0.0f; + + // Top Left + AddImageGradient( + drawList, + g_noiseTexture.get(), + min, + center, + ImVec2(UV_0 * UV_MAX.x, 0.0f * UV_MAX.y), + ImVec2(0.5f * UV_MAX.x, 0.5f * UV_MAX.y), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTL_gradTL, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTL_gradTR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTL_gradBR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTL_gradBL, time)), + true); + + // Top Right + AddImageGradient( + drawList, + g_noiseTexture.get(), + topCenter, + centerRight, + ImVec2(0.5f * UV_MAX.x, 0.0f * UV_MAX.y), + ImVec2(UV_1 * UV_MAX.x, 0.5f * UV_MAX.y), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTR_gradTL, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTR_gradTR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTR_gradBR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseTR_gradBL, time)), + true); + + // Bottom Right + AddImageGradient( + drawList, + g_noiseTexture.get(), + center, + max, + ImVec2(0.5f * UV_MAX.x, 0.5f * UV_MAX.y), + ImVec2(UV_1 * UV_MAX.x, 1.0f * UV_MAX.y), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBR_gradTL, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBR_gradTR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBR_gradBR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBR_gradBL, time)), + true); + + // Bottom Left + AddImageGradient( + drawList, + g_noiseTexture.get(), + centerLeft, + bottomCenter, + ImVec2(UV_0 * UV_MAX.x, 0.5f * UV_MAX.y), + ImVec2(0.5f * UV_MAX.x, 1.0f * UV_MAX.y), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBL_gradTL, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBL_gradTR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBL_gradBR, time)), + IM_COL32(255, 255, 255, 255 * Sample(g_noiseBL_gradBL, time)), + true); +} + +void TVStatic::Init() +{ + g_flashTexture = LOAD_ZSTD_TEXTURE(g_options_static_flash); + g_noiseTexture = LOAD_ZSTD_TEXTURE(g_options_static); +} + +static float ComputeTime(double appearTime) +{ + return (ImGui::GetTime() - appearTime) + FRAME_OFFSET * FRAME_SCALE; +} + +float TVStatic::ComputeThumbnailAlpha(double appearTime) +{ + return Sample(g_thumbnailAlpha, ComputeTime(appearTime)); +} + +void TVStatic::Draw(const ImVec2& center, const ImVec2& resolution, double appearTime) +{ + float time = ComputeTime(appearTime); + + DrawStatic(center, resolution, time); + DrawFlash(center, resolution, time); +} diff --git a/UnleashedRecomp/ui/tv_static.h b/UnleashedRecomp/ui/tv_static.h new file mode 100644 index 0000000..3cf8306 --- /dev/null +++ b/UnleashedRecomp/ui/tv_static.h @@ -0,0 +1,8 @@ +#pragma once + +struct TVStatic +{ + static void Init(); + static float ComputeThumbnailAlpha(double appearTime); + static void Draw(const ImVec2& center, const ImVec2& resolution, double appearTime); +}; diff --git a/UnleashedRecompResources b/UnleashedRecompResources index a2c4daf..5ba3baa 160000 --- a/UnleashedRecompResources +++ b/UnleashedRecompResources @@ -1 +1 @@ -Subproject commit a2c4daf8f7f0b0ff4386933c4d872fa47239f0e4 +Subproject commit 5ba3baac5a62203d6c77c9476d7dc8f14adcdc10