From 9172d954b496798cc7503facaa42bcb44b5c78c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Wed, 24 Jun 2026 09:32:48 +0000 Subject: [PATCH 01/18] Adding font awesome at tag 7.2.0 as a submodule --- .gitmodules | 3 +++ vendor/github.com/FortAwesome/Font-Awesome | 1 + 2 files changed, 4 insertions(+) create mode 160000 vendor/github.com/FortAwesome/Font-Awesome diff --git a/.gitmodules b/.gitmodules index c0ad80e..9f61868 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "vendor/github.com/carbonengine/vcpkg-registry"] path = vendor/github.com/carbonengine/vcpkg-registry url = git@github.com:carbonengine/vcpkg-registry.git +[submodule "vendor/github.com/FortAwesome/Font-Awesome"] + path = vendor/github.com/FortAwesome/Font-Awesome + url = https://github.com/FortAwesome/Font-Awesome diff --git a/vendor/github.com/FortAwesome/Font-Awesome b/vendor/github.com/FortAwesome/Font-Awesome new file mode 160000 index 0000000..337dd20 --- /dev/null +++ b/vendor/github.com/FortAwesome/Font-Awesome @@ -0,0 +1 @@ +Subproject commit 337dd2045d5621ce0f8567c33c256f3dedeed55d From 0cd1e43078a7b304a8bb0cce673b7f61d6ea68cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Thu, 25 Jun 2026 12:19:33 +0000 Subject: [PATCH 02/18] Downgrading font-awesome submodule to 6.7.2 --- vendor/github.com/FortAwesome/Font-Awesome | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/github.com/FortAwesome/Font-Awesome b/vendor/github.com/FortAwesome/Font-Awesome index 337dd20..af62053 160000 --- a/vendor/github.com/FortAwesome/Font-Awesome +++ b/vendor/github.com/FortAwesome/Font-Awesome @@ -1 +1 @@ -Subproject commit 337dd2045d5621ce0f8567c33c256f3dedeed55d +Subproject commit af620534bfc3c2d4cbefcfeec29603bbe7809e64 From 0a23102a4bff357dc13895a0fef9539c6b5959be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Thu, 25 Jun 2026 12:21:32 +0000 Subject: [PATCH 03/18] Fontawesome integration Added a script that takes the ttf font file and converts it into a binary header file. It also generates defines so we can actually use it in c++ code effectively. Added a build script to run the fontawesome generation when the script/font changes --- src/viewer/CMakeLists.txt | 18 +++ src/viewer/scripts/fontAwesomeCreator.py | 186 +++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 src/viewer/scripts/fontAwesomeCreator.py diff --git a/src/viewer/CMakeLists.txt b/src/viewer/CMakeLists.txt index 0153d0c..e66b165 100644 --- a/src/viewer/CMakeLists.txt +++ b/src/viewer/CMakeLists.txt @@ -23,6 +23,8 @@ set( SHADER_BINARY_TO_SHADER_CACHE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/shaderCac set( SOURCE_SHADER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/assets/shaders/) set( GENERATED_SHADER_DIR ${CMAKE_CURRENT_BINARY_DIR}/GeneratedShaders) set( GLSLC_EXECUTABLE "${VCPKG_INSTALLED_PATH}/tools/shaderc/glslc.exe") +set( FONT_AWESOME_GENERATOR ${CMAKE_CURRENT_SOURCE_DIR}/scripts/fontAwesomeCreator.py) +set( GENERATED_FONTAWESOME_DIR ${CMAKE_CURRENT_BINARY_DIR}/GeneratedFontAwesome) set( ENV{VK_LAYER_PATH} "${VCPKG_INSTALLED_PATH}/bin") set( ENV{VULKAN_SDK} "${VCPKG_INSTALLED_PATH}") @@ -80,6 +82,7 @@ ccp_enable_vs_analizers(CarbonMeshViewer) set(COMPILED_SHADERS "") +# Compile shaders to SPIR-V and generate header files foreach( SHADER_PATH ${SHADERS} ) file( RELATIVE_PATH SHADER_RELATIVE_PATH ${SOURCE_SHADER_DIR} ${SHADER_PATH} ) @@ -100,6 +103,7 @@ foreach( SHADER_PATH ${SHADERS} ) endif() endforeach() +# shader cache generation add_custom_command( OUTPUT "${GENERATED_SHADER_DIR}/generatedShaderCache.h" DEPENDS ${SHADERS} ${COMPILED_SHADERS} ${SHADER_BINARY_TO_SHADER_CACHE} @@ -114,9 +118,23 @@ message(STATUS "generated files: ${COMPILED_SHADERS}") add_custom_target(CompileShaders DEPENDS ${COMPILED_SHADERS}) add_dependencies(CarbonMeshViewer CompileShaders) +# font awesome generation +get_filename_component(FONT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/github.com/FortAwesome/Font-Awesome" ABSOLUTE) + +add_custom_command( + OUTPUT "${GENERATED_FONTAWESOME_DIR}/faBinary.h" "${GENERATED_FONTAWESOME_DIR}/faLookup.h" + DEPENDS ${FONT_AWESOME_GENERATOR} "${FONT_DIR}/webfonts/fa-solid-900.ttf" "${FONT_DIR}/metadata/icons.json" + + COMMAND ${Python3_EXECUTABLE} ${FONT_AWESOME_GENERATOR} "${FONT_DIR}/webfonts/fa-solid-900.ttf" "${FONT_DIR}/metadata/icons.json" "${GENERATED_FONTAWESOME_DIR}/faBinary.h" "${GENERATED_FONTAWESOME_DIR}/faLookup.h" + COMMENT "Generating Font Awesome header ${GENERATED_FONTAWESOME_DIR}/faBinary.h and ${GENERATED_FONTAWESOME_DIR}/faLookup.h from ${CMAKE_CURRENT_SOURCE_DIR} and ${FONT_DIR}." +) +add_custom_target(CompileFontAwesome DEPENDS "${GENERATED_FONTAWESOME_DIR}/faBinary.h" "${GENERATED_FONTAWESOME_DIR}/faLookup.h") +add_dependencies(CarbonMeshViewer CompileFontAwesome) + target_link_libraries(CarbonMeshViewer PUBLIC CarbonMesh CcpMath glfw Vulkan::Vulkan imgui::imgui tinyfiledialogs::tinyfiledialogs) target_precompile_headers(CarbonMeshViewer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/stdafx.h) target_include_directories(CarbonMeshViewer PRIVATE ${GENERATED_SHADER_DIR}) +target_include_directories(CarbonMeshViewer PRIVATE ${GENERATED_FONTAWESOME_DIR}) target_compile_options(CarbonMeshViewer PRIVATE $<$,$>:-fms-extensions>) target_compile_features(CarbonMeshViewer PRIVATE cxx_std_17) diff --git a/src/viewer/scripts/fontAwesomeCreator.py b/src/viewer/scripts/fontAwesomeCreator.py new file mode 100644 index 0000000..40c283f --- /dev/null +++ b/src/viewer/scripts/fontAwesomeCreator.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © 2025 CCP ehf. + +"""Generate ImGui-readable Font Awesome headers. + +This script has two purposes: +1) Generate a font byte-data header consumable by Dear ImGui. +2) Generate a Font Awesome icon define header for use in C++ UI code. + +The output style intentionally mirrors the spirit of +https://github.com/ocornut/imgui/blob/master/misc/fonts/binary_to_compressed_c.cpp +by emitting a `unsigned int` data array and explicit byte size. +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path +from typing import Dict, Iterable, List, Tuple + + +def _find_repo_root() -> Path: + """Resolve repository root from this script location.""" + return Path(__file__).resolve().parents[3] + + +def _chunked(values: Iterable[int], chunk_size: int) -> Iterable[List[int]]: + chunk: List[int] = [] + for value in values: + chunk.append(value) + if len(chunk) == chunk_size: + yield chunk + chunk = [] + if chunk: + yield chunk + + +def _bytes_to_u32_words(data: bytes) -> List[int]: + words: List[int] = [] + for i in range(0, len(data), 4): + block = data[i : i + 4] + value = 0 + for byte_index, byte_value in enumerate(block): + value |= byte_value << (byte_index * 8) + words.append(value) + return words + + +def _format_word_array(words: List[int], per_line: int = 12) -> str: + formatted_lines: List[str] = [] + for chunk in _chunked(words, per_line): + joined = ", ".join(f"0x{value:08x}" for value in chunk) + formatted_lines.append(f" {joined},") + return "\n".join(formatted_lines) + + +def _sanitize_macro_suffix(icon_name: str) -> str: + upper = icon_name.upper().replace("-", "_") + upper = re.sub(r"[^A-Z0-9_]", "_", upper) + upper = re.sub(r"_+", "_", upper).strip("_") + return upper if upper else "UNKNOWN" + + +def _codepoint_to_utf8_hex_escapes(codepoint: int) -> str: + encoded = chr(codepoint).encode("utf-8") + return "".join(f"\\x{byte:02x}" for byte in encoded) + + +def _load_icons(icons_json_path: Path) -> List[Tuple[str, int]]: + with icons_json_path.open("r", encoding="utf-8") as input_file: + icon_data: Dict[str, Dict[str, object]] = json.load(input_file) + + icons: List[Tuple[str, int]] = [] + for icon_name, payload in icon_data.items(): + styles = payload.get("styles", []) + unicode_hex = payload.get("unicode") + if "solid" not in styles or not isinstance(unicode_hex, str): + continue + + codepoint = int(unicode_hex, 16) + icons.append((icon_name, codepoint)) + + icons.sort(key=lambda item: item[0]) + return icons + + +def _build_font_header(ttf_bytes: bytes, ttf_path: Path) -> str: + words = _bytes_to_u32_words(ttf_bytes) + array_body = _format_word_array(words) + + return ( + "// Auto-generated by fontAwesomeCreator.py.\n" + "// Source TTF: " + f"{ttf_path.as_posix()}\n" + "// Dear ImGui usage example:\n" + "// io.Fonts->AddFontFromMemoryTTF(\n" + "// const_cast(fa_solid_900_ttf_data),\n" + "// static_cast(fa_solid_900_ttf_size),\n" + "// 16.0f, &config, iconRanges);\n" + "#pragma once\n\n" + "static const unsigned int fa_solid_900_ttf_size = " + f"{len(ttf_bytes)}u;\n" + f"static const unsigned int fa_solid_900_ttf_data[{len(words)}] =\n" + "{\n" + f"{array_body}\n" + "};\n" + ) + + +def _build_icon_define_header(icons: List[Tuple[str, int]], icons_path: Path) -> str: + if not icons: + raise ValueError("No solid icons were found in the input icons metadata.") + + min_cp = min(codepoint for _, codepoint in icons) + max_cp = max(codepoint for _, codepoint in icons) + + lines: List[str] = [ + "// Auto-generated by fontAwesomeCreator.py.", + f"// Source icons.json: {icons_path.as_posix()}", + "#pragma once", + "", + f"#define ICON_MIN_FA 0x{min_cp:04x}", + f"#define ICON_MAX_FA 0x{max_cp:04x}", + "", + ] + + for icon_name, codepoint in icons: + macro_suffix = _sanitize_macro_suffix(icon_name) + utf8_escaped = _codepoint_to_utf8_hex_escapes(codepoint) + lines.append(f"#define ICON_FA_{macro_suffix} \"{utf8_escaped}\"") + + lines.append("") + return "\n".join(lines) + + +def _parse_args(argv: List[str]) -> Tuple[Path, Path, Path, Path]: + """ + Supported forms: + - fontAwesomeCreator.py + """ + if len(argv) == 5: + input_ttf = Path(argv[1]).resolve() + icons_json = Path(argv[2]).resolve() + output_font_header = Path(argv[3]).resolve() + output_define_header = Path(argv[4]).resolve() + return input_ttf, icons_json, output_font_header, output_define_header + + usage = ( + "Usage:\n" + " python fontAwesomeCreator.py \n" + " python fontAwesomeCreator.py \n" + " python fontAwesomeCreator.py " + ) + raise ValueError(usage) + + +def main(argv: List[str]) -> int: + try: + input_ttf, icons_json, out_font_header, out_define_header = _parse_args(argv) + + if not input_ttf.is_file(): + raise FileNotFoundError(f"Input TTF not found: {input_ttf}") + if not icons_json.is_file(): + raise FileNotFoundError(f"Input icons JSON not found: {icons_json}") + + out_font_header.parent.mkdir(parents=True, exist_ok=True) + out_define_header.parent.mkdir(parents=True, exist_ok=True) + + ttf_bytes = input_ttf.read_bytes() + icons = _load_icons(icons_json) + + out_font_header.write_text(_build_font_header(ttf_bytes, input_ttf), encoding="utf-8") + out_define_header.write_text(_build_icon_define_header(icons, icons_json), encoding="utf-8") + except Exception as exc: # broad catch to keep the generator easy to diagnose from CMake logs + print(str(exc)) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) From bebe9594ba1902d9f5ca509098894db81e8a9e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 26 Jun 2026 09:58:10 +0000 Subject: [PATCH 04/18] Added better sizing of fontawesome buttons And added a strike through button (for this is not active) --- src/viewer/rendering/uiRenderer.cpp | 82 ++++++++++++++++++------ src/viewer/rendering/uiRenderer.h | 3 +- src/viewer/scripts/fontAwesomeCreator.py | 63 ++++++++++++++---- 3 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/viewer/rendering/uiRenderer.cpp b/src/viewer/rendering/uiRenderer.cpp index a7e35d3..d805e73 100644 --- a/src/viewer/rendering/uiRenderer.cpp +++ b/src/viewer/rendering/uiRenderer.cpp @@ -15,15 +15,22 @@ #include "vulkan/vulkanerrors.h" #include +#include + + +const float MENU_BAR_HEIGHT = 18.0f; +const float ANIMATION_PLAYER_HEIGHT = 36.0f; +const float BUTTON_WIDTH = 19.0f; +const float BUTTON_HEIGHT = 19.0f; +const float FONT_AWESOME_SIZE = 13.0f; // ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file // NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) -// -// taken from https://github.com/ocornut/imgui/issues/2644 + namespace ImGui { -// threeway checkbox +// threeway checkbox taken from https://github.com/ocornut/imgui/issues/2644 bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ) { bool ret; @@ -49,11 +56,35 @@ bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ) } return ret; } -}; -const float MENU_BAR_HEIGHT = 18.0f; -const float ANIMATION_PLAYER_HEIGHT = 36.0f; -const float BUTTON_SIZE = 18.0f; +bool FontAwesomeButton( const FaIcon& icon, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) +{ + const float faButtonPadding = 4.0f; + const float glyphWidth = FONT_AWESOME_SIZE * icon.xyRatio; + const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); + ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); + bool ret = ImGui::Button( icon.text, ImVec2( width, height ) ); + ImGui::PopStyleVar(); + + return ret; +} + +bool FontAwesomeSlashedButton( const FaIcon& icon, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) +{ + bool ret = ImGui::FontAwesomeButton( icon, width, height ); + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImGui::GetItemRectMax(); + float slashPadding = 1.5f; + ImGui::GetWindowDrawList()->AddLine( + ImVec2( min.x + slashPadding, max.y - slashPadding ), + ImVec2( max.x - slashPadding, min.y + slashPadding ), + ImGui::GetColorU32( ImGuiCol_Text ), + 2.0f ); + + return ret; +} + +} UIRenderer::UIRenderer( std::shared_ptr renderer ) : m_renderer( renderer ), @@ -88,7 +119,14 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.Fonts->AddFontDefault(); + + static const ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + ImFontConfig config; + config.MergeMode = true; + // add fontawesome icons + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, fa_solid_900_ttf_size, FONT_AWESOME_SIZE, &config, iconRanges ); // Setup Dear ImGui style ImGui::StyleColorsDark(); @@ -668,7 +706,7 @@ void UIRenderer::SetupMorphTarget( const MorphTargetUiState& morphTarget, size_t { ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, BUTTON_SIZE ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, BUTTON_WIDTH ); ImGui::TableNextRow(); ImGui::TableNextColumn(); @@ -701,7 +739,7 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s if( ImGui::CollapsingHeader( header.c_str(), ImGuiTreeNodeFlags_None ) ) { ImGui::BeginDisabled(); - ImGui::Button( "+", ImVec2( BUTTON_SIZE, BUTTON_SIZE ) ); + ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, ImGui::GetContentRegionAvail().x ); ImGui::SetItemTooltip( "Adds an animation override - Disabled since model doesn't have a skeleton" ); ImGui::EndDisabled(); } @@ -743,7 +781,8 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s ImGui::EndTable(); ImGui::SeparatorText( "Animation Owners" ); - OnChange( ImGui::Button( "+", ImVec2( ImGui::GetContentRegionAvail().x, BUTTON_SIZE ) ), [this, &appState]() { + bool pressed = ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, ImGui::GetContentRegionAvail().x ); + OnChange( pressed, [this, &appState]() { auto* path = this->FileOpenDialog(); if( path != nullptr ) { @@ -754,7 +793,6 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s } } } ); - ImGui::SetItemTooltip( "Adds an animation owner from a cmf file" ); for( int32_t index = 0; index < skeletonOwners.size(); ++index ) @@ -788,8 +826,8 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s if( index != 0 ) { - ImGui::SameLine( ImGui::GetContentRegionAvail().x - BUTTON_SIZE ); - OnChange( ImGui::Button( "-", ImVec2( BUTTON_SIZE, BUTTON_SIZE ) ), [&appState, skeletonOwner, index]() { + ImGui::SameLine( ImGui::GetContentRegionAvail().x - BUTTON_WIDTH ); + OnChange( ImGui::FontAwesomeButton( ICON_FA_SQUARE_MINUS ), [&appState, skeletonOwner, index]() { appState.modelState.animationOverrides.RemoveAt( index - 1 ); if( appState.modelState.activeAnimationOwner.GetValue() == skeletonOwner.cmfContent ) @@ -923,13 +961,13 @@ void UIRenderer::ToggleUiVisibility() m_showMainUI = !m_showMainUI; } -const char* UIRenderer::GetPlaybackButtonLabel() const +const FaIcon& UIRenderer::GetPlaybackButtonIcon() const { if( m_playback.playing ) { - return "Pause"; + return ICON_FA_PAUSE; } - return "Play"; + return ICON_FA_PLAY; } void UIRenderer::HandlePlaybackButtonPressed() @@ -971,21 +1009,23 @@ void UIRenderer::SetupPlaybackControls( AppState& appState ) { ImGui::BeginDisabled(); } - OnChange( ImGui::Button( "<", ImVec2( 18.0f, 18.0f ) ), [this, &appState]() { + OnChange( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_LEFT ), [this, &appState]() { StepAnimation( -0.1f, appState ); } ); - ImGui::SameLine(); - OnChange( ImGui::Button( GetPlaybackButtonLabel(), ImVec2( 64.0f, 18.0f ) ), [this]() { HandlePlaybackButtonPressed(); } ); + OnChange( ImGui::FontAwesomeButton( GetPlaybackButtonIcon() ), [this]() { HandlePlaybackButtonPressed(); } ); ImGui::SameLine(); - OnChange( ImGui::Button( ">", ImVec2( 18.0f, 18.0f ) ), [this, &appState]() { + OnChange( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_RIGHT ), [this, &appState]() { StepAnimation( 0.1f, appState ); } ); ImGui::SameLine(); ImGui::PushItemWidth( 30.0f ); - ImGui::Checkbox( "##repeatanimation", &m_playback.repeat ); + bool repeatPressed = m_playback.repeat ? ImGui::FontAwesomeButton( ICON_FA_REPEAT ) : ImGui::FontAwesomeSlashedButton( ICON_FA_REPEAT ); + OnChange( repeatPressed, [this]() { + m_playback.repeat = !m_playback.repeat; + } ); ImGui::SetItemTooltip( "Repeat the animation" ); ImGui::SameLine(); diff --git a/src/viewer/rendering/uiRenderer.h b/src/viewer/rendering/uiRenderer.h index 9676ddc..124b29b 100644 --- a/src/viewer/rendering/uiRenderer.h +++ b/src/viewer/rendering/uiRenderer.h @@ -5,6 +5,7 @@ // used by the template file #include #include +#include #include "appState.h" #include "rendering/renderer.h" @@ -152,7 +153,7 @@ class UIRenderer void SetupPlaybackControls( AppState& appState ); void SetupPopupWindows( AppState& appState ); - const char* GetPlaybackButtonLabel() const; + const FaIcon& GetPlaybackButtonIcon() const; void HandlePlaybackButtonPressed(); void StepAnimation( float amount, AppState& appState ); diff --git a/src/viewer/scripts/fontAwesomeCreator.py b/src/viewer/scripts/fontAwesomeCreator.py index 40c283f..22b7dc2 100644 --- a/src/viewer/scripts/fontAwesomeCreator.py +++ b/src/viewer/scripts/fontAwesomeCreator.py @@ -16,7 +16,6 @@ from __future__ import annotations import json -import os import re import sys from pathlib import Path @@ -70,11 +69,46 @@ def _codepoint_to_utf8_hex_escapes(codepoint: int) -> str: return "".join(f"\\x{byte:02x}" for byte in encoded) -def _load_icons(icons_json_path: Path) -> List[Tuple[str, int]]: +def _format_ratio(width: int, height: int) -> str: + if height == 0: + return "1.0f" + + ratio = width / height + formatted = f"{ratio:.6g}" + if "." not in formatted: + formatted += ".0" + return f"{formatted}f" + + +def _read_icon_size(payload: Dict[str, object]) -> Tuple[int, int]: + svg_payload = payload.get("svg") + if not isinstance(svg_payload, dict): + return 0, 0 + + solid_payload = svg_payload.get("solid") + if not isinstance(solid_payload, dict): + return 0, 0 + + width = solid_payload.get("width") + height = solid_payload.get("height") + if isinstance(width, int) and isinstance(height, int): + return width, height + + view_box = solid_payload.get("viewBox") + if isinstance(view_box, list) and len(view_box) >= 4: + view_box_width = view_box[2] + view_box_height = view_box[3] + + return float(view_box_width), float(view_box_height) + + return 0, 0 + + +def _load_icons(icons_json_path: Path) -> List[Tuple[str, int, int, int]]: with icons_json_path.open("r", encoding="utf-8") as input_file: icon_data: Dict[str, Dict[str, object]] = json.load(input_file) - icons: List[Tuple[str, int]] = [] + icons: List[Tuple[str, int, int, int]] = [] for icon_name, payload in icon_data.items(): styles = payload.get("styles", []) unicode_hex = payload.get("unicode") @@ -82,7 +116,8 @@ def _load_icons(icons_json_path: Path) -> List[Tuple[str, int]]: continue codepoint = int(unicode_hex, 16) - icons.append((icon_name, codepoint)) + width, height = _read_icon_size(payload) + icons.append((icon_name, codepoint, width, height)) icons.sort(key=lambda item: item[0]) return icons @@ -111,12 +146,12 @@ def _build_font_header(ttf_bytes: bytes, ttf_path: Path) -> str: ) -def _build_icon_define_header(icons: List[Tuple[str, int]], icons_path: Path) -> str: +def _build_icon_define_header(icons: List[Tuple[str, int, int, int]], icons_path: Path) -> str: if not icons: raise ValueError("No solid icons were found in the input icons metadata.") - min_cp = min(codepoint for _, codepoint in icons) - max_cp = max(codepoint for _, codepoint in icons) + min_cp = min(codepoint for _, codepoint, _, _ in icons) + max_cp = max(codepoint for _, codepoint, _, _ in icons) lines: List[str] = [ "// Auto-generated by fontAwesomeCreator.py.", @@ -125,13 +160,21 @@ def _build_icon_define_header(icons: List[Tuple[str, int]], icons_path: Path) -> "", f"#define ICON_MIN_FA 0x{min_cp:04x}", f"#define ICON_MAX_FA 0x{max_cp:04x}", + "static const unsigned short ICON_RANGE_FA[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };", + "", + "struct FaIcon", + "{", + "\tconst char* text;", + "\tfloat xyRatio;", + "};", "", ] - for icon_name, codepoint in icons: + for icon_name, codepoint, width, height in icons: macro_suffix = _sanitize_macro_suffix(icon_name) utf8_escaped = _codepoint_to_utf8_hex_escapes(codepoint) - lines.append(f"#define ICON_FA_{macro_suffix} \"{utf8_escaped}\"") + ratio = _format_ratio(width, height) + lines.append(f"static const FaIcon ICON_FA_{macro_suffix} = {{ \"{utf8_escaped}\", {ratio} }};") lines.append("") return "\n".join(lines) @@ -151,8 +194,6 @@ def _parse_args(argv: List[str]) -> Tuple[Path, Path, Path, Path]: usage = ( "Usage:\n" - " python fontAwesomeCreator.py \n" - " python fontAwesomeCreator.py \n" " python fontAwesomeCreator.py " ) raise ValueError(usage) From 473182027e6769ecae9b5f883477ef9c6ce93087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 26 Jun 2026 10:50:22 +0000 Subject: [PATCH 05/18] Disabling items when no model is loaded --- src/viewer/rendering/uiRenderer.cpp | 12 ++++-------- src/viewer/rendering/uiRenderer_template_impl.h | 5 +++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/viewer/rendering/uiRenderer.cpp b/src/viewer/rendering/uiRenderer.cpp index d805e73..cf56488 100644 --- a/src/viewer/rendering/uiRenderer.cpp +++ b/src/viewer/rendering/uiRenderer.cpp @@ -341,6 +341,7 @@ void UIRenderer::SetupGeneralView( AppState& appState ) ImGui::Text( "%zu", m_uiState.modelStates.meshes.size() ); ImGui::TableNextRow(); + ImGui::BeginDisabled( appState.modelState.meshes.empty() ); ImGui::TableNextColumn(); ImGui::Text( "Polygon Mode" ); ImGui::TableNextColumn(); @@ -391,6 +392,7 @@ void UIRenderer::SetupGeneralView( AppState& appState ) } ); } ); ImGui::TableNextRow(); + ImGui::EndDisabled(); bool hasAudioOcclusionMeshes = std::any_of( m_uiState.modelStates.meshes.begin(), m_uiState.modelStates.meshes.end(), []( const MeshUiState& state ) { return state.hasAudioOcclusionMesh; @@ -549,10 +551,7 @@ void UIRenderer::SetupVertexAxisRows( MeshState& meshAppState ) void UIRenderer::SetupVertexAxisRow( StateCollection>& vertexAxisStates, const char* label, int maxUsageIndex ) { - if( vertexAxisStates.empty() ) - { - ImGui::BeginDisabled(); - } + ImGui::BeginDisabled( vertexAxisStates.empty() ); ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted( label ); @@ -590,10 +589,7 @@ void UIRenderer::SetupVertexAxisRow( StateCollection>& } ImGui::NewLine(); } - if( vertexAxisStates.empty() ) - { - ImGui::EndDisabled(); - } + ImGui::EndDisabled(); } void UIRenderer::SetupMeshView( const MeshUiState& mesh, AppState& appState ) diff --git a/src/viewer/rendering/uiRenderer_template_impl.h b/src/viewer/rendering/uiRenderer_template_impl.h index 4a6b865..0caf32d 100644 --- a/src/viewer/rendering/uiRenderer_template_impl.h +++ b/src/viewer/rendering/uiRenderer_template_impl.h @@ -86,5 +86,10 @@ void UIRenderer::SetupModelAxisRow( std::vector Date: Mon, 29 Jun 2026 13:44:37 +0000 Subject: [PATCH 06/18] Add camera focus state and dynamic focus menu options Introduced cameraFocus state to AppState for targeting specific bounding spheres. Updated camera trigger handling to use focusSphere. Enhanced the Camera menu to allow focusing on the whole model or individual meshes. --- src/viewer/appState.cpp | 1 + src/viewer/appState.h | 1 + src/viewer/rendering/camera.cpp | 19 ++++++--- src/viewer/rendering/camera.h | 2 +- src/viewer/rendering/uiRenderer.cpp | 63 +++++++++++++++++++++++------ 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/viewer/appState.cpp b/src/viewer/appState.cpp index 69a4c53..604498c 100644 --- a/src/viewer/appState.cpp +++ b/src/viewer/appState.cpp @@ -17,6 +17,7 @@ void AppState::CallStateCallbacks() windowSize.CallCallbacks( *this ); mouseState.CallCallbacks( *this ); cameraTrigger.CallCallbacks( *this ); + cameraFocus.CallCallbacks( *this ); cmfContent.CallCallbacks( *this ); cmfPath.CallCallbacks( *this ); diff --git a/src/viewer/appState.h b/src/viewer/appState.h index 5a87922..a392f23 100644 --- a/src/viewer/appState.h +++ b/src/viewer/appState.h @@ -189,6 +189,7 @@ struct AppState // camera State cameraTrigger{ CameraTrigger::CAMERA_TRIGGER_NONE }; + State cameraFocus{ {} }; // cmf State> cmfContent{ nullptr }; diff --git a/src/viewer/rendering/camera.cpp b/src/viewer/rendering/camera.cpp index 76c65ec..79603af 100644 --- a/src/viewer/rendering/camera.cpp +++ b/src/viewer/rendering/camera.cpp @@ -11,8 +11,9 @@ void Camera::Initialize( AppState& state ) SetScreenSize( width, height ); } ); - state.cameraTrigger.RegisterCallback( [this]( CameraTrigger trigger, AppState& ) { - HandleCameraTriggerChange( trigger ); + state.cameraTrigger.RegisterCallback( [this]( CameraTrigger trigger, AppState& state ) { + CcpMath::Sphere focusSphere = state.cameraFocus.GetValue(); + HandleCameraTriggerChange( trigger, focusSphere ); } ); state.mouseState.RegisterCallback( [this]( MouseState newMouseState, AppState& ) { @@ -30,17 +31,25 @@ void Camera::Initialize( AppState& state ) SetScreenSize( width, height ); } -void Camera::HandleCameraTriggerChange( CameraTrigger& trigger ) +void Camera::HandleCameraTriggerChange( CameraTrigger& trigger, const CcpMath::Sphere& focusSphere ) { if( trigger == CameraTrigger::CAMERA_TRIGGER_NONE ) { return; } + + CcpMath::Sphere boundingSphere = m_boundingSphere; + if( focusSphere.radius > 0.0f ) + { + boundingSphere = focusSphere; + } + this->LookAt( boundingSphere.center ); + + switch( trigger ) { case CameraTrigger::CAMERA_TRIGGER_FOCUS: - this->LookAt( this->m_boundingSphere.center ); - m_targetZoom = this->m_boundingSphere.radius * DEFAULT_ZOOM_MULTIPLIER; + m_targetZoom = boundingSphere.radius * DEFAULT_ZOOM_MULTIPLIER; break; case CameraTrigger::CAMERA_TRIGGER_LOOK_UP: this->m_targetRotation = RotationQuaternion( 0.0f, -PI / 2.0f, 0.0f ); diff --git a/src/viewer/rendering/camera.h b/src/viewer/rendering/camera.h index 102d4dc..c789b50 100644 --- a/src/viewer/rendering/camera.h +++ b/src/viewer/rendering/camera.h @@ -14,7 +14,7 @@ class Camera void Initialize( AppState& state ); - void HandleCameraTriggerChange( CameraTrigger& trigger ); + void HandleCameraTriggerChange( CameraTrigger& trigger, const CcpMath::Sphere& focusSphere ); void HandleMouseStateChanged( MouseState& newMouseState ); diff --git a/src/viewer/rendering/uiRenderer.cpp b/src/viewer/rendering/uiRenderer.cpp index cf56488..f171f39 100644 --- a/src/viewer/rendering/uiRenderer.cpp +++ b/src/viewer/rendering/uiRenderer.cpp @@ -22,6 +22,7 @@ const float MENU_BAR_HEIGHT = 18.0f; const float ANIMATION_PLAYER_HEIGHT = 36.0f; const float BUTTON_WIDTH = 19.0f; const float BUTTON_HEIGHT = 19.0f; +const ImVec2 BUTTON_SIZE = ImVec2( BUTTON_WIDTH, BUTTON_HEIGHT ); const float FONT_AWESOME_SIZE = 13.0f; // ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file @@ -57,21 +58,23 @@ bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ) return ret; } -bool FontAwesomeButton( const FaIcon& icon, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) +bool FontAwesomeButton( const FaIcon& icon, int id = 0, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) { const float faButtonPadding = 4.0f; const float glyphWidth = FONT_AWESOME_SIZE * icon.xyRatio; const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); + ImGui::PushID( id ); bool ret = ImGui::Button( icon.text, ImVec2( width, height ) ); + ImGui::PopID(); ImGui::PopStyleVar(); return ret; } -bool FontAwesomeSlashedButton( const FaIcon& icon, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) +bool FontAwesomeSlashedButton( const FaIcon& icon, int id = 0, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) { - bool ret = ImGui::FontAwesomeButton( icon, width, height ); + bool ret = ImGui::FontAwesomeButton( icon, id, width, height ); ImVec2 min = ImGui::GetItemRectMin(); ImVec2 max = ImGui::GetItemRectMax(); float slashPadding = 1.5f; @@ -281,6 +284,7 @@ void UIRenderer::SetupUi( AppState& appState ) void UIRenderer::CMFInfoWindow( AppState& appState ) { bool open = true; + float width = (float)appState.windowSize.GetValue().first; float height = (float)appState.windowSize.GetValue().second; @@ -596,9 +600,9 @@ void UIRenderer::SetupMeshView( const MeshUiState& mesh, AppState& appState ) { if( ImGui::TreeNode( mesh.name.c_str() ) ) { + auto& meshAppState = appState.modelState.meshes[mesh.meshIndex].GetValue(); if( ImGui::BeginTable( "##table", 2 ) ) { - auto& meshAppState = appState.modelState.meshes[mesh.meshIndex].GetValue(); ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); ImGui::TableNextRow(); @@ -735,7 +739,7 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s if( ImGui::CollapsingHeader( header.c_str(), ImGuiTreeNodeFlags_None ) ) { ImGui::BeginDisabled(); - ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, ImGui::GetContentRegionAvail().x ); + ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, 0, ImGui::GetContentRegionAvail().x ); ImGui::SetItemTooltip( "Adds an animation override - Disabled since model doesn't have a skeleton" ); ImGui::EndDisabled(); } @@ -777,7 +781,7 @@ void UIRenderer::SetupSkeletonOwners( const std::vector& s ImGui::EndTable(); ImGui::SeparatorText( "Animation Owners" ); - bool pressed = ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, ImGui::GetContentRegionAvail().x ); + bool pressed = ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, 0, ImGui::GetContentRegionAvail().x ); OnChange( pressed, [this, &appState]() { auto* path = this->FileOpenDialog(); if( path != nullptr ) @@ -904,14 +908,43 @@ void UIRenderer::SetupMenubar( AppState& appState ) } ImGui::EndMenu(); } + auto content = appState.cmfContent.GetValue(); + if( ImGui::BeginMenu( "View" ) ) { - if( ImGui::BeginMenu( "Camera" ) ) + if( ImGui::BeginMenu( "Camera", content != nullptr ) ) { - if( ImGui::MenuItem( "Focus", "Ctrl+F" ) ) + std::vector> focusObjects; + if( content != nullptr ) { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); + const auto& meshes = content->m_cmfData->meshes; + + if( meshes.size() > 1 ) + { + focusObjects.push_back( { "Whole Model", content->GetBoundingSphere() } ); + } + std::for_each( meshes.begin(), meshes.end(), [&focusObjects]( const cmf::Mesh& mesh ) { + focusObjects.push_back( { cmf::ToStdString( mesh.name ), CcpMath::Sphere( mesh.bounds ) } ); + } ); } + + uint32_t index = 0; + for( const auto& [menuName, sphere] : focusObjects ) + { + std::string menuLabel = "Focus on " + menuName; + const char* shortcut = nullptr; + if( index == 0 ) + { + shortcut = "Ctrl+F"; + } + if( ImGui::MenuItem( menuLabel.c_str(), shortcut ) ) + { + appState.cameraFocus.ForceSetValue( sphere ); + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); + } + ++index; + } + ImGui::Separator(); if( ImGui::MenuItem( "Look Right (+X)" ) ) { appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_RIGHT ); @@ -936,7 +969,6 @@ void UIRenderer::SetupMenubar( AppState& appState ) { appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_BACK ); } - ImGui::EndMenu(); } ImGui::Separator(); @@ -1131,10 +1163,15 @@ void UIRenderer::UpdateInputs( AppState& appState ) appState.cmfPath.SetValue( std::string( filePath ) ); } } - if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_F ) ) + const auto content = appState.cmfContent.GetValue(); + if( content != nullptr ) { - // focus camera on model - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); + if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_F ) ) + { + // focus camera on model + appState.cameraFocus.ForceSetValue( content->GetBoundingSphere() ); + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); + } } if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_F12 ) ) { From 8324001ce8a5d5c70325132b5a8aba92b3c1af4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Mon, 29 Jun 2026 15:29:56 +0000 Subject: [PATCH 07/18] Adding a changed to chaderCacheCreator that somehow got forgotten in the whole open source move --- src/viewer/scripts/shaderCacheCreator.py | 255 ++++++++++++++--------- 1 file changed, 151 insertions(+), 104 deletions(-) diff --git a/src/viewer/scripts/shaderCacheCreator.py b/src/viewer/scripts/shaderCacheCreator.py index 41fb8f0..023a333 100644 --- a/src/viewer/scripts/shaderCacheCreator.py +++ b/src/viewer/scripts/shaderCacheCreator.py @@ -1,118 +1,165 @@ -# Copyright © 2025 CCP ehf. - -import sys import os import re +import sys + + +VERTEX_DECLARATION_PATTERN = re.compile( + r"^\s*layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+\w+\s+(\w+)\s*;" +) + +MULTI_USAGE_VERTEX_NAME = re.compile( + r"^\s*#pragma\s+multi_usage\s+(\w+)\s*$" +) + +SHADER_USAGE_LOOKUP = { + "inposition": "cmf::Usage::Position", + "innormal": "cmf::Usage::Normal", + "inbinormal": "cmf::Usage::Binormal", + "intangent": "cmf::Usage::Tangent", + "inpackedtangents": "cmf::Usage::PackedTangent", + "inpackedtangentslegacy": "cmf::Usage::PackedTangentLegacy", + "incolor": "cmf::Usage::Color", + "intexcoord": "cmf::Usage::TexCoord", + "inboneindices": "cmf::Usage::BoneIndices", + "inboneweights": "cmf::Usage::BoneWeights", +} + + +def shader_wrapper(shader_name): + return 'Shader( {\n #include "%s"\n } )' % shader_name + + +def parse_vertex_declarations(shader_path): + declarations = [] + multi_usage_names = [] + + with open(shader_path, "r") as vertex_shader: + for line in vertex_shader: + multi_usage_names_match = MULTI_USAGE_VERTEX_NAME.match(line) + if multi_usage_names_match: + multi_usage_names.append(multi_usage_names_match.group(1).lower()) + continue + + match = VERTEX_DECLARATION_PATTERN.match(line) + if not match: + continue + + + variable_name = match.group(2).lower() + if variable_name not in SHADER_USAGE_LOOKUP: + raise KeyError( + "could not find '%s' in applicable vertex variable names %s" + % (variable_name, SHADER_USAGE_LOOKUP.keys()) + ) + + declarations.append((int(match.group(1)), SHADER_USAGE_LOOKUP[variable_name], variable_name in multi_usage_names)) + + if len(multi_usage_names) > 1: + print("ERROR: Shader '%s' has multi usage index elements: %s" % (os.path.basename(shader_path), multi_usage_names)) + print("Only zero or one elements can be multi index usage") + sys.exit(1) + + return declarations + + +def collect_vertex_declarations(source_dir): + vertex_declarations = {} + + for filename in sorted(os.listdir(source_dir)): + if filename.endswith(".vert"): + shader_name = filename.rsplit(".", 1)[0] + vertex_declarations[shader_name] = parse_vertex_declarations(os.path.join(source_dir, filename)) -def enumerate_files( sourceDir, binaryDir, output): - shaderGroups = {} - vertexDeclarations = {} - - # matches the pattern: - # layout( location = 0 ) in vec3 inPosition; - # grabs the 0 and inPosition - pattern = r'layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+\w+\s+(\w+);' - - shaderUsageLookup = { - "inposition": "cmf::Usage::Position", - "innormal": "cmf::Usage::Normal", - "inbinormal": "cmf::Usage::Binormal", - "intangent": "cmf::Usage::Tangent", - "inpackedtangents": "cmf::Usage::PackedTangent", - "inpackedtangentslegacy": "cmf::Usage::PackedTangentLegacy", - "incolor": "cmf::Usage::Color", - "intexcoord": "cmf::Usage::TexCoord", - "inboneindices": "cmf::Usage::BoneIndices", - "inboneweights": "cmf::Usage::BoneWeights" - }; - - # find all the input vertex declarations of vertex shaders - for filename in os.listdir(sourceDir): - if not filename.endswith('.vert'): + return vertex_declarations + + +def collect_shader_groups(binary_dir): + shader_groups = {} + + for filename in sorted(os.listdir(binary_dir)): + if not filename.endswith(".spv.h"): continue - - parts = filename.split('.') - name = parts[0] - - with open(os.path.join(sourceDir, filename), 'r') as vertexShader: - inputDeclarations = [] - for line in vertexShader: - match = re.search(pattern, line) - if match: - variableName = str(match.group(2)).lower() - try: - usage = shaderUsageLookup[variableName] - except KeyError as e: - print("could not find '%s' in applicable vertex variable names %s" % ( variableName, shaderUsageLookup.keys() )) - raise e - - inputDeclarations.append( ( int(match.group(1)), usage )) - vertexDeclarations[name] = inputDeclarations - for filename in os.listdir(binaryDir): - if not filename.endswith('.spv.h'): + + parts = filename.split(".") + if len(parts) < 3: continue - parts = filename.split('.') - name = parts[0] - shaderType = parts[1] - shaders = shaderGroups.get(name, [None, None, None]) - if shaderType == 'vert': - shaders[0] = filename - elif shaderType == 'frag': - shaders[1] = filename - elif shaderType == 'comp': - shaders[2] = filename - - shaderGroups[name] = shaders - - code = "" - - for name, shaders in shaderGroups.items(): - - if (not shaders[0] or not shaders[1]) and not shaders[2]: - print("Warning: Shader group '%s' is missing a shader." % name) + shader_name = parts[0] + shader_type = parts[1] + group = shader_groups.setdefault(shader_name, {"vert": None, "frag": None, "comp": None}) + + if shader_type in group: + group[shader_type] = filename + + return shader_groups + + +def display_name(shader_name): + if not shader_name.startswith("model_"): + return shader_name + + parts = shader_name[len("model_"):].split("_") + return " ".join(part[:1].upper() + part[1:] for part in parts if part) + + +def format_input_layout(shader_name, vertex_declarations): + entries = vertex_declarations.get(shader_name, []) + return ",\n".join("{%d, %s, %d}" % (location, usage, int(multi_usage)) for location, usage, multi_usage in entries) + + +def format_shader_entry(shader_name, shader_group, vertex_declarations): + vert_shader = "std::nullopt" + frag_shader = "std::nullopt" + comp_shader = "std::nullopt" + + if shader_group["vert"]: + vert_shader = shader_wrapper(shader_group["vert"]) + if shader_group["frag"]: + frag_shader = shader_wrapper(shader_group["frag"]) + if shader_group["comp"]: + comp_shader = shader_wrapper(shader_group["comp"]) + + return ( + ' {"%s", { \n' + ' %s,\n' + ' %s,\n' + ' %s,\n' + ' %s,\n' + ' { %s }\n' + ' } \n' + ' },' + % ( + display_name(shader_name), + vert_shader, + frag_shader, + comp_shader, + str(shader_name.startswith("model_")).lower(), + format_input_layout(shader_name, vertex_declarations), + ) + ) + + +def enumerate_files(source_dir, binary_dir, output_file): + vertex_declarations = collect_vertex_declarations(source_dir) + shader_groups = collect_shader_groups(binary_dir) + + entries = [] + for shader_name in sorted(shader_groups.keys()): + shader_group = shader_groups[shader_name] + + if (not shader_group["vert"] or not shader_group["frag"]) and not shader_group["comp"]: + print("Warning: Shader group '%s' is missing a shader." % shader_name) continue - vertShader = "std::nullopt" - fragShader = "std::nullopt" - compShader = "std::nullopt" - if shaders[0]: - vertShader = """Shader( { - #include \"%s\" - } - )""" % ( shaders[0], ) - if shaders[1]: - fragShader = """Shader( { - #include \"%s\" - } )""" % shaders[1] - if shaders[2]: - compShader = """Shader( { - #include \"%s\" - } )""" % shaders[2] - - - splitNames = name.replace("model_", "").split("_") - displayName = name - if name.startswith("model_"): - displayName = " ".join(s[0].upper() + s[1:] for s in splitNames) - - code += """ -{\"%s\", { - %s, - %s, - %s, - %s, - { %s } - } -},""" % (displayName, vertShader, fragShader, compShader, str(name.startswith("model_")).lower(), ",\n".join(["{%d, %s}" % (index, usage) for (index, usage) in vertexDeclarations.get(name, {}) ])) - - code = code[:-1] - with open(output, 'w') as f: - f.write(code) + entries.append(format_shader_entry(shader_name, shader_group, vertex_declarations)) + code = "\n".join(entries) + + with open(output_file, "w") as f: + f.write(code) if __name__ == '__main__': if len(sys.argv) != 4: print("Usage: shaderCacheCreator.py ") sys.exit(1) - enumerate_files(sys.argv[1], sys.argv[2], sys.argv[3]) + enumerate_files(sys.argv[1], sys.argv[2], sys.argv[3]) \ No newline at end of file From acedf4d40dc200cf7032e5f283dab744239756d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Mon, 29 Jun 2026 16:03:18 +0000 Subject: [PATCH 08/18] Refactor polygon mode to be per-mesh, update UI and effects Remove global polygon mode state and UI; polygon mode is now determined per-mesh based on mesh topology. Refactor MeshRenderable and GraphicsEffect to support this, replacing SetShaderName/SetConfig with a new Reset method. Update UI to remove polygon mode selection and improve disabled state handling for missing mesh/model data. --- src/viewer/appState.h | 2 -- .../rendering/models/primitiveEffects.cpp | 6 ++-- src/viewer/rendering/renderable/mesh.cpp | 27 +++++++---------- src/viewer/rendering/renderable/mesh.h | 2 +- src/viewer/rendering/uiRenderer.cpp | 30 ++++++------------- src/viewer/rendering/uiRenderer.h | 1 - .../rendering/uiRenderer_template_impl.h | 8 +++-- .../rendering/vulkan/graphicseffect.cpp | 10 +++++++ src/viewer/rendering/vulkan/graphicseffect.h | 1 + 9 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/viewer/appState.h b/src/viewer/appState.h index a392f23..1e31d6a 100644 --- a/src/viewer/appState.h +++ b/src/viewer/appState.h @@ -148,7 +148,6 @@ struct ModelState State> activeShader{ {} }; State>> availableShaders{ {} }; - State polygonMode{ VK_POLYGON_MODE_FILL }; State currentAnimation{ "" }; State currentAnimationTime{ 0.0f }; @@ -166,7 +165,6 @@ struct ModelState meshes.CallCallbacks( appState ); activeShader.CallCallbacks( appState ); availableShaders.CallCallbacks( appState ); - polygonMode.CallCallbacks( appState ); currentAnimation.CallCallbacks( appState ); currentAnimationTime.CallCallbacks( appState ); boneDebug.CallCallbacks( appState ); diff --git a/src/viewer/rendering/models/primitiveEffects.cpp b/src/viewer/rendering/models/primitiveEffects.cpp index 146e900..832c6be 100644 --- a/src/viewer/rendering/models/primitiveEffects.cpp +++ b/src/viewer/rendering/models/primitiveEffects.cpp @@ -93,8 +93,7 @@ GraphicsEffect PrimitiveEffects::CreateFlatColorEffect( std::shared_ptr( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 2, vertexToBoneMapping.data(), vertexToBoneMapping.size() ); // vertex to bone mapping effect.RegisterStorageBuffer( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 3, 0xFF ); // selected bones effect.RegisterUniformData( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 4, colorInfo ); - effect.SetConfig( config ); - effect.SetShaderName( "flatcolor" ); + effect.Reset( "flatcolor", config ); return effect; } @@ -120,8 +119,7 @@ GraphicsEffect PrimitiveEffects::CreateOrientationEffect( std::shared_ptr( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 0 ); effect.RegisterStorageBuffer( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 1, 0xFF ); - effect.SetConfig( config ); - effect.SetShaderName( "orientationgizmo" ); + effect.Reset( "orientationgizmo", config ); return effect; } diff --git a/src/viewer/rendering/renderable/mesh.cpp b/src/viewer/rendering/renderable/mesh.cpp index 0e3870f..3691a66 100644 --- a/src/viewer/rendering/renderable/mesh.cpp +++ b/src/viewer/rendering/renderable/mesh.cpp @@ -38,21 +38,19 @@ MeshRenderable::MeshRenderable( std::shared_ptr data, const cmf::Mes } } } + + m_polygonMode = m_cmfMesh.topology == cmf::MeshTopology::TriangleList ? VK_POLYGON_MODE_FILL : VK_POLYGON_MODE_POINT; + m_topology = m_cmfMesh.topology == cmf::MeshTopology::TriangleList ? VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST : VK_PRIMITIVE_TOPOLOGY_POINT_LIST; } void MeshRenderable::Initialize( AppState& appState ) { m_prepass.Initialize( appState ); - appState.modelState.polygonMode.RegisterCallback( [this]( VkPolygonMode mode, AppState& appState ) { - auto [name, declaration] = appState.modelState.activeShader.GetValue(); - SetRenderingMode( name, declaration, mode ); - } ); - appState.modelState.activeShader.RegisterCallback( [this]( std::pair shaderInputDeclaration, AppState& appState ) { auto [name, declaration] = shaderInputDeclaration; - SetRenderingMode( name, declaration, appState.modelState.polygonMode.GetValue() ); + SetRenderingMode( name, declaration ); } ); m_meshIndex = appState.modelState.meshes.AddState( [this]( MeshState& meshState ) { @@ -400,24 +398,23 @@ void MeshRenderable::DrawIndexed( GraphicsCommandBuffer& commandBuffer ) } } -VkResult MeshRenderable::SetRenderingMode( std::string shaderName, GraphicsEffectTypes::ShaderInputDeclaration shaderInputDeclaration, VkPolygonMode polygonMode ) +VkResult MeshRenderable::SetRenderingMode( std::string shaderName, GraphicsEffectTypes::ShaderInputDeclaration shaderInputDeclaration ) { auto logicalDevice = m_renderer->GetDevice()->GetLogicalDevice(); - m_polygonMode = polygonMode; m_shaderName = shaderName; CR_RETURN( vkDeviceWaitIdle( logicalDevice ) ); auto config = GraphicsEffectTypes::Config(); config.topology = m_topology; - config.polygonMode = polygonMode; - config.cullMode = ( polygonMode == VK_POLYGON_MODE_FILL ) ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE; + config.polygonMode = m_polygonMode; + config.cullMode = ( m_polygonMode == VK_POLYGON_MODE_FILL ) ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE; config.inputDeclaration.vertexDeclarations = shaderInputDeclaration.vertexDeclarations; config.inputDeclaration.stride = m_stride; - m_modelEffect.SetShaderName( m_shaderName ); - m_modelEffect.SetConfig( config ); + m_modelEffect.Reset( m_shaderName, config ); + if( !m_modelEffect.IsInitialized() ) { m_modelEffect.RegisterUniformData( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 0 ); @@ -443,8 +440,7 @@ VkResult MeshRenderable::SetRenderingMode( std::string shaderName, GraphicsEffec 3, 0 } }; - m_wireframeEffect.SetShaderName( "wireframeoverlay" ); - m_wireframeEffect.SetConfig( wireframeConfig ); + m_wireframeEffect.Reset( "wireframeoverlay", wireframeConfig ); m_wireframeEffect.RegisterUniformData( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 0 ); m_wireframeEffect.Initialize(); } @@ -469,8 +465,7 @@ GraphicsEffect MeshRenderable::GetAudioOcclusionEffect( std::shared_ptr( VkShaderStageFlagBits::VK_SHADER_STAGE_VERTEX_BIT, 0 ); return effect; } diff --git a/src/viewer/rendering/renderable/mesh.h b/src/viewer/rendering/renderable/mesh.h index 947b7a7..c7a7052 100644 --- a/src/viewer/rendering/renderable/mesh.h +++ b/src/viewer/rendering/renderable/mesh.h @@ -22,7 +22,7 @@ class MeshRenderable void Render( GraphicsCommandBuffer& commandBuffer, const AppState& appState, const Camera& camera ); void RenderDebug( GraphicsCommandBuffer& commandBuffer, const AppState& appState, const Camera& camera ); void PrepareMesh( ComputeCommandBuffer& computeCommandBuffer ); - VkResult SetRenderingMode( std::string shaderName, GraphicsEffectTypes::ShaderInputDeclaration shaderInputDeclaration, VkPolygonMode polygonMode ); + VkResult SetRenderingMode( std::string shaderName, GraphicsEffectTypes::ShaderInputDeclaration shaderInputDeclaration ); void SetAnimation( const cmf::Animation* animation ); void UpdateMeshCurves( float animationTime, const cmf::Animation* animation, AppState& appState ); diff --git a/src/viewer/rendering/uiRenderer.cpp b/src/viewer/rendering/uiRenderer.cpp index f171f39..179edce 100644 --- a/src/viewer/rendering/uiRenderer.cpp +++ b/src/viewer/rendering/uiRenderer.cpp @@ -344,14 +344,7 @@ void UIRenderer::SetupGeneralView( AppState& appState ) ImGui::TableNextColumn(); ImGui::Text( "%zu", m_uiState.modelStates.meshes.size() ); ImGui::TableNextRow(); - ImGui::BeginDisabled( appState.modelState.meshes.empty() ); - ImGui::TableNextColumn(); - ImGui::Text( "Polygon Mode" ); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); - SetupCombo( "##polygonmode", m_uiState.polygonModeComboBox, appState.modelState.polygonMode ); - ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text( "Visualization" ); @@ -593,6 +586,15 @@ void UIRenderer::SetupVertexAxisRow( StateCollection>& } ImGui::NewLine(); } + else + { + ImGui::BeginDisabled( true ); + bool value = false; + ImGui::Checkbox( ( std::string( "##" ) + label ).c_str(), &value ); + ImGui::SetItemTooltip( "Mesh doesn't have %s", label ); + ImGui::SameLine(); + ImGui::EndDisabled(); + } ImGui::EndDisabled(); } @@ -1195,13 +1197,6 @@ void UIRenderer::UpdateUiState( AppState& appState ) if( cmfContent != nullptr ) { m_uiState.filePath = appState.cmfPath.GetValue(); - // polygon mode selection - m_uiState.polygonModeComboBox.items = { - { "Fill", VK_POLYGON_MODE_FILL }, - { "Line", VK_POLYGON_MODE_LINE }, - { "Point", VK_POLYGON_MODE_POINT } - }; - m_uiState.polygonModeComboBox.SetSelectedItemByValue( appState.modelState.polygonMode.GetValue() ); size_t meshIndex = 0; size_t morphIndex = 0; @@ -1356,13 +1351,6 @@ void UIRenderer::UpdateUiState( AppState& appState ) m_playback.animationComboBox.SetSelectedItemByValue( appState.modelState.currentAnimation.GetValue() ); } - // polygon mode selection - m_uiState.polygonModeComboBox.items = { - { "Fill", VK_POLYGON_MODE_FILL }, - { "Line", VK_POLYGON_MODE_LINE }, - { "Point", VK_POLYGON_MODE_POINT } - }; - m_uiState.polygonModeComboBox.SetSelectedItemByValue( appState.modelState.polygonMode.GetValue() ); m_uiState.filePath = "No file loaded"; // visualization shader selection diff --git a/src/viewer/rendering/uiRenderer.h b/src/viewer/rendering/uiRenderer.h index 124b29b..ef25dbd 100644 --- a/src/viewer/rendering/uiRenderer.h +++ b/src/viewer/rendering/uiRenderer.h @@ -118,7 +118,6 @@ class UIRenderer std::string filePath; ModelUiState modelStates{}; std::vector skeletonOwners{}; - CmfUiComboBox polygonModeComboBox; CmfUiComboBox> visualizationShaderComboBox; bool boneDebug{ false }; bool jointDebug{ false }; diff --git a/src/viewer/rendering/uiRenderer_template_impl.h b/src/viewer/rendering/uiRenderer_template_impl.h index 0caf32d..b272bce 100644 --- a/src/viewer/rendering/uiRenderer_template_impl.h +++ b/src/viewer/rendering/uiRenderer_template_impl.h @@ -88,8 +88,12 @@ void UIRenderer::SetupModelAxisRow( std::vector renderer ) : void GraphicsEffect::SetConfig( GraphicsEffectTypes::Config config ) { m_config = config; + if( m_initialized ) + { + m_initialized = RecreatePipeline() == VK_SUCCESS; + } +} + +void GraphicsEffect::Reset( std::string name, GraphicsEffectTypes::Config config ) +{ + m_config = config; + m_shaderName = name; if( m_initialized ) { diff --git a/src/viewer/rendering/vulkan/graphicseffect.h b/src/viewer/rendering/vulkan/graphicseffect.h index 4e49616..e69bbf4 100644 --- a/src/viewer/rendering/vulkan/graphicseffect.h +++ b/src/viewer/rendering/vulkan/graphicseffect.h @@ -16,6 +16,7 @@ class GraphicsEffect : public Effect Matrix view; }; + void Reset( std::string name, GraphicsEffectTypes::Config config ); void SetConfig( GraphicsEffectTypes::Config config ); void Bind( VkCommandBuffer commandBuffer, uint32_t currentFrameIndex ) override; size_t GetStride() const; From ecf03cbf0f416252d971271704e89694d1562eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Tue, 30 Jun 2026 13:11:27 +0000 Subject: [PATCH 09/18] Refactor camera near/far plane calculation logic Update Camera::GetProjection() to use a minimum far plane of 1.0f and set the near plane relative to the far plane, improving projection matrix stability and depth range handling. PLAT-11626 --- src/viewer/rendering/camera.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/viewer/rendering/camera.cpp b/src/viewer/rendering/camera.cpp index 79603af..7ca7d8a 100644 --- a/src/viewer/rendering/camera.cpp +++ b/src/viewer/rendering/camera.cpp @@ -110,11 +110,13 @@ void Camera::HandleMouseStateChanged( MouseState& mouseState ) Matrix Camera::GetProjection() const { - const float ABSOLUTE_MIN_NEAR_PLANE = 0.01f; + const float ABSOLUTE_MIN_FAR_PLANE = 1.0f; const float boundingDepth = m_boundingSphere.radius * 2.0f; const float centerDepth = -TransformCoord( m_boundingSphere.center, GetView() ).z; - const float nearPlane = std::max( ABSOLUTE_MIN_NEAR_PLANE, centerDepth - boundingDepth ); - const float farPlane = std::max( nearPlane + ABSOLUTE_MIN_NEAR_PLANE, centerDepth + boundingDepth ); + const float calculatedFar = centerDepth + boundingDepth; + const float farPlane = std::max( ABSOLUTE_MIN_FAR_PLANE, calculatedFar ); + + const float nearPlane = std::max( farPlane / 100.0f , centerDepth - boundingDepth ); return PerspectiveFovMatrix( m_fov, From 3f1a3807b870ef496abbd9ad9ffe965ad341e39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Tue, 30 Jun 2026 15:18:19 +0000 Subject: [PATCH 10/18] Moving all ui rendering to a ui folder --- src/viewer/application.h | 2 +- src/viewer/rendering/{ => ui}/uiDetailWindow.cpp | 0 src/viewer/rendering/{ => ui}/uiDetailWindow.h | 2 +- src/viewer/rendering/{ => ui}/uiRenderer.cpp | 2 +- src/viewer/rendering/{ => ui}/uiRenderer.h | 2 +- src/viewer/rendering/{ => ui}/uiRenderer_template_impl.h | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename src/viewer/rendering/{ => ui}/uiDetailWindow.cpp (100%) rename src/viewer/rendering/{ => ui}/uiDetailWindow.h (98%) rename src/viewer/rendering/{ => ui}/uiRenderer.cpp (99%) rename src/viewer/rendering/{ => ui}/uiRenderer.h (99%) rename src/viewer/rendering/{ => ui}/uiRenderer_template_impl.h (100%) diff --git a/src/viewer/application.h b/src/viewer/application.h index 98e7386..e9f2e03 100644 --- a/src/viewer/application.h +++ b/src/viewer/application.h @@ -7,7 +7,7 @@ #include "rendering/orientationGizmoRenderer.h" #include "rendering/sceneRenderer.h" #include "rendering/renderer.h" -#include "rendering/uiRenderer.h" +#include "rendering/ui/uiRenderer.h" class Application { diff --git a/src/viewer/rendering/uiDetailWindow.cpp b/src/viewer/rendering/ui/uiDetailWindow.cpp similarity index 100% rename from src/viewer/rendering/uiDetailWindow.cpp rename to src/viewer/rendering/ui/uiDetailWindow.cpp diff --git a/src/viewer/rendering/uiDetailWindow.h b/src/viewer/rendering/ui/uiDetailWindow.h similarity index 98% rename from src/viewer/rendering/uiDetailWindow.h rename to src/viewer/rendering/ui/uiDetailWindow.h index 9deec9e..8b35531 100644 --- a/src/viewer/rendering/uiDetailWindow.h +++ b/src/viewer/rendering/ui/uiDetailWindow.h @@ -2,7 +2,7 @@ #pragma once -#include "../appState.h" +#include "../../appState.h" class UIDetailWindow { diff --git a/src/viewer/rendering/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp similarity index 99% rename from src/viewer/rendering/uiRenderer.cpp rename to src/viewer/rendering/ui/uiRenderer.cpp index 179edce..bf206af 100644 --- a/src/viewer/rendering/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -12,7 +12,7 @@ #include #include "appState.h" -#include "vulkan/vulkanerrors.h" +#include "../vulkan/vulkanerrors.h" #include #include diff --git a/src/viewer/rendering/uiRenderer.h b/src/viewer/rendering/ui/uiRenderer.h similarity index 99% rename from src/viewer/rendering/uiRenderer.h rename to src/viewer/rendering/ui/uiRenderer.h index ef25dbd..c2b06b3 100644 --- a/src/viewer/rendering/uiRenderer.h +++ b/src/viewer/rendering/ui/uiRenderer.h @@ -10,7 +10,7 @@ #include "appState.h" #include "rendering/renderer.h" #include "uiDetailWindow.h" -#include "vulkan/commandbuffer.h" +#include "../vulkan/commandbuffer.h" namespace ImGui { diff --git a/src/viewer/rendering/uiRenderer_template_impl.h b/src/viewer/rendering/ui/uiRenderer_template_impl.h similarity index 100% rename from src/viewer/rendering/uiRenderer_template_impl.h rename to src/viewer/rendering/ui/uiRenderer_template_impl.h From c956dc0f769583a7d20c0f7139cbc9ac8808febb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 11:56:26 +0000 Subject: [PATCH 11/18] Refactor UI into modular components Split monolithic UIRenderer into focused modules: uiGeneralWindow, uiMenubar, uiAnimationPlayback, uiCustomWidgets, and uiConsts. Moved UI logic into these components for better separation of concerns and maintainability. Introduced MenuState for UI visibility and unified file loading via cmfLoadRequest. Modernized code with improved state management, input handling, and C++ idioms. SceneRenderer no longer manages cmfContent callbacks --- src/viewer/appState.cpp | 2 + src/viewer/appState.h | 2 + src/viewer/application.cpp | 14 +- src/viewer/main.cpp | 8 +- src/viewer/rendering/sceneRenderer.cpp | 4 - .../rendering/ui/uiAnimationPlayback.cpp | 155 +++ src/viewer/rendering/ui/uiAnimationPlayback.h | 21 + src/viewer/rendering/ui/uiConsts.h | 12 + src/viewer/rendering/ui/uiCustomWidgets.cpp | 111 ++ src/viewer/rendering/ui/uiCustomWidgets.h | 62 + src/viewer/rendering/ui/uiGeneralWindow.cpp | 698 ++++++++++ src/viewer/rendering/ui/uiGeneralWindow.h | 47 + src/viewer/rendering/ui/uiMenubar.cpp | 124 ++ src/viewer/rendering/ui/uiMenubar.h | 16 + src/viewer/rendering/ui/uiRenderer.cpp | 1141 +---------------- src/viewer/rendering/ui/uiRenderer.h | 137 +- .../rendering/ui/uiRenderer_template_impl.h | 99 -- 17 files changed, 1286 insertions(+), 1367 deletions(-) create mode 100644 src/viewer/rendering/ui/uiAnimationPlayback.cpp create mode 100644 src/viewer/rendering/ui/uiAnimationPlayback.h create mode 100644 src/viewer/rendering/ui/uiConsts.h create mode 100644 src/viewer/rendering/ui/uiCustomWidgets.cpp create mode 100644 src/viewer/rendering/ui/uiCustomWidgets.h create mode 100644 src/viewer/rendering/ui/uiGeneralWindow.cpp create mode 100644 src/viewer/rendering/ui/uiGeneralWindow.h create mode 100644 src/viewer/rendering/ui/uiMenubar.cpp create mode 100644 src/viewer/rendering/ui/uiMenubar.h diff --git a/src/viewer/appState.cpp b/src/viewer/appState.cpp index 604498c..ab502bc 100644 --- a/src/viewer/appState.cpp +++ b/src/viewer/appState.cpp @@ -21,6 +21,7 @@ void AppState::CallStateCallbacks() cmfContent.CallCallbacks( *this ); cmfPath.CallCallbacks( *this ); + cmfLoadRequest.CallCallbacks( *this ); exitRequested.CallCallbacks( *this ); modelState.CallCallbacks( *this ); @@ -30,3 +31,4 @@ void AppState::ResetModelState() { modelState = {}; } + diff --git a/src/viewer/appState.h b/src/viewer/appState.h index 1e31d6a..e9cccde 100644 --- a/src/viewer/appState.h +++ b/src/viewer/appState.h @@ -192,6 +192,8 @@ struct AppState // cmf State> cmfContent{ nullptr }; State cmfPath{ "" }; + // requests from ui to load cmf data + State cmfLoadRequest{ "" }; State exitRequested{ false }; diff --git a/src/viewer/application.cpp b/src/viewer/application.cpp index b57e0ce..460acec 100644 --- a/src/viewer/application.cpp +++ b/src/viewer/application.cpp @@ -150,6 +150,14 @@ void Application::Initialize() m_renderer->Resize( width, height ); } ); + m_appState.cmfLoadRequest.RegisterCallback( [this]( std::string path, AppState& ) { + if( !path.empty() ) + { + LoadCmfFile( path ); + m_appState.cmfLoadRequest.SetValueNoCallback( "" ); + } + } ); + m_valid = true; } @@ -163,6 +171,7 @@ void Application::Run() while( !glfwWindowShouldClose( m_window ) && !m_appState.exitRequested.GetValue() ) { + m_appState.CallStateCallbacks(); // Poll for and process events @@ -197,8 +206,9 @@ void Application::Run() m_sceneRenderer->Render( m_appState, m_camera ); m_orientationGizmoRenderer->Render( m_appState, m_camera ); - + m_uiRenderer->Update( m_appState ); + m_uiRenderer->Render( m_appState ); if( m_renderer->EndRender() != VK_SUCCESS ) @@ -241,10 +251,10 @@ void Application::LoadCmfFile( const std::string& path ) else { Log::Info( "Applying model file %s", path.c_str() ); - // load the cmf content from the path and set it in the app state m_appState.cmfPath.SetValue( path ); m_appState.modelState.activeAnimationOwner.SetValue( data ); m_appState.cmfContent.ForceSetValue( data ); + m_sceneRenderer->SetData( data, m_appState ); } } diff --git a/src/viewer/main.cpp b/src/viewer/main.cpp index 0614cdd..6c66d95 100644 --- a/src/viewer/main.cpp +++ b/src/viewer/main.cpp @@ -22,8 +22,12 @@ int main( int argc, char** argv ) app.Initialize(); if( argc > 1 ) - { - app.LoadCmfFile( argv[1] ); + { + for( int i = 1; i < argc; ++i ) + { + std::cout << "Loading file: " << argv[i] << std::endl; + app.LoadCmfFile( argv[i] ); + } } app.Run(); diff --git a/src/viewer/rendering/sceneRenderer.cpp b/src/viewer/rendering/sceneRenderer.cpp index 43f0ad0..915dc77 100644 --- a/src/viewer/rendering/sceneRenderer.cpp +++ b/src/viewer/rendering/sceneRenderer.cpp @@ -31,10 +31,6 @@ SceneRenderer::~SceneRenderer() VkResult SceneRenderer::Initialize( AppState& state ) { - state.cmfContent.RegisterCallback( [this]( std::shared_ptr content, AppState& appState ) { - this->SetData( content, appState ); - } ); - state.windowSize.RegisterCallback( [this]( std::pair size, AppState& appState ) { auto [width, height] = size; this->m_graphicsCommandBuffer.SetRenderSize( width, height ); diff --git a/src/viewer/rendering/ui/uiAnimationPlayback.cpp b/src/viewer/rendering/ui/uiAnimationPlayback.cpp new file mode 100644 index 0000000..2555eb5 --- /dev/null +++ b/src/viewer/rendering/ui/uiAnimationPlayback.cpp @@ -0,0 +1,155 @@ +// Copyright © 2026 CCP ehf. + +#include "uiAnimationPlayback.h" +#include + +#include "uiCustomWidgets.h" + +const float ANIMATION_SELECTION_WIDTH = 120.0f; + +void UIAnimationPlayback::Render( AppState& appState ) +{ + UpdateState( appState ); + + const auto& [width, height] = appState.windowSize.GetValue(); + + ImGui::SetNextWindowPos( ImVec2( 0, static_cast( height ) - UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); + ImGui::SetNextWindowSize( ImVec2( static_cast( width ), UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); + if( ImGui::Begin( "##animationTitle", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize ) ) + { + // animation selection + ImGui::PushItemWidth( ANIMATION_SELECTION_WIDTH ); + + std::vector animationNames; + std::vector animationDuration; + const auto& activeAnimationOwner = appState.modelState.activeAnimationOwner.GetValue(); + int32_t selectedAnimationIndex = -1; + + if( activeAnimationOwner != nullptr && !activeAnimationOwner->m_cmfData->animations.empty() ) + { + // add the rest pose + animationNames.push_back( "Rest Pose" ); + animationDuration.push_back( 0.0f ); + const auto& animations = activeAnimationOwner->m_cmfData->animations; + std::for_each( animations.begin(), animations.end(), [&animationNames, &animationDuration]( const cmf::Animation& animation ) { + animationDuration.push_back( animation.duration ); + animationNames.push_back( cmf::ToStdString( animation.name ) ); + } ); + // Map currentAnimation to index: 0 = Rest Pose (""), 1..N = animations[0..N-1] + if( appState.modelState.currentAnimation.GetValue().empty() ) + { + selectedAnimationIndex = 0; + } + else + { + auto it = std::find_if( animations.begin(), animations.end(), [&appState]( const cmf::Animation& animation ) { + return cmf::ToStdString( animation.name ) == appState.modelState.currentAnimation.GetValue(); + } ); + if( it != animations.end() ) + { + selectedAnimationIndex = static_cast( std::distance( animations.begin(), it ) ) + 1; + } + else + { + // couldn't find the animation, reset to rest pose + selectedAnimationIndex = 0; + m_playing = false; + m_currentTime = 0.0f; + m_duration = 0.0f; + appState.modelState.currentAnimation.SetValue( "" ); + appState.modelState.currentAnimationTime.SetValue( m_currentTime ); + } + } + } + + ImGui::BeginDisabled( animationNames.empty() ); + + ImGui::ComboBox( "##animation", animationNames, selectedAnimationIndex, [&]( int32_t selectedIndex ) { + m_playing = false; + m_currentTime = 0.0f; + m_duration = animationDuration[selectedIndex]; + if( selectedIndex == 0 ) + { + // rest pose + appState.modelState.currentAnimation.SetValue( "" ); + } + else + { + appState.modelState.currentAnimation.SetValue( animationNames[selectedIndex] ); + } + appState.modelState.currentAnimationTime.SetValue( m_currentTime ); + } ); + ImGui::SetItemTooltip( "Select an animation to play" ); + + ImGui::SameLine(); + + // step left + if( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_LEFT, 0 ) ) + { + m_currentTime -= 0.1f; + } + ImGui::SameLine(); + + // play/pause button + if( ImGui::FontAwesomeButton( m_playing ? ICON_FA_PAUSE : ICON_FA_PLAY, 1 ) ) + { + m_playing = !m_playing; + + if( m_currentTime == m_duration ) + { + m_currentTime = 0.0f; + } + } + + ImGui::SameLine(); + + // step right + if( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_RIGHT, 2 ) ) + { + m_currentTime += 0.1f; + } + + ImGui::SameLine(); + bool repeatPressed = m_repeat ? ImGui::FontAwesomeButton( ICON_FA_REPEAT, 3 ) : ImGui::FontAwesomeSlashedButton( ICON_FA_REPEAT, 3 ); + if( repeatPressed ) + { + m_repeat = !m_repeat; + } + ImGui::SetItemTooltip( "Repeat the animation" ); + + ImGui::SameLine(); + float availableWidth = ImGui::GetContentRegionAvail().x; + ImGui::PushItemWidth( availableWidth ); + if( ImGui::SliderFloat( "##playbackSlider", &m_currentTime, 0.0f, m_duration ) ) + { + appState.modelState.currentAnimationTime.SetValue( m_currentTime ); + } + ImGui::SetItemTooltip( "%.3fs / %.3fs", m_currentTime, m_duration ); + ImGui::PopItemWidth(); + + ImGui::EndDisabled(); + ImGui::SameLine(); + } + ImGui::End(); +} + +void UIAnimationPlayback::UpdateState( AppState& appState ) +{ + if( m_playing ) + { + m_currentTime += ImGui::GetIO().DeltaTime; + if( m_currentTime >= m_duration ) + { + if( m_repeat ) + { + m_currentTime = 0.0f; + } + else + { + m_currentTime = m_duration; + m_playing = false; + } + } + } + appState.modelState.currentAnimationTime.SetValue( m_currentTime ); +} \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiAnimationPlayback.h b/src/viewer/rendering/ui/uiAnimationPlayback.h new file mode 100644 index 0000000..e4a9772 --- /dev/null +++ b/src/viewer/rendering/ui/uiAnimationPlayback.h @@ -0,0 +1,21 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "../../appState.h" + + + +class UIAnimationPlayback +{ +public: + void Render( AppState& appState ); + +private: + void UpdateState( AppState& appState ); + + float m_duration{ 0.0f }; + float m_currentTime{ 0.0f }; + bool m_playing{ false }; + bool m_repeat{ false }; +}; \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiConsts.h b/src/viewer/rendering/ui/uiConsts.h new file mode 100644 index 0000000..a73448c --- /dev/null +++ b/src/viewer/rendering/ui/uiConsts.h @@ -0,0 +1,12 @@ +#pragma once +#include + +namespace UiConsts +{ +const float MENU_BAR_HEIGHT = 18.0f; +const float ANIMATION_PLAYER_HEIGHT = 36.0f; +const float BUTTON_WIDTH = 19.0f; +const float BUTTON_HEIGHT = 19.0f; +const ImVec2 BUTTON_SIZE = ImVec2( BUTTON_WIDTH, BUTTON_HEIGHT ); +const float FONT_AWESOME_SIZE = 13.0f; +} \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiCustomWidgets.cpp b/src/viewer/rendering/ui/uiCustomWidgets.cpp new file mode 100644 index 0000000..b870a40 --- /dev/null +++ b/src/viewer/rendering/ui/uiCustomWidgets.cpp @@ -0,0 +1,111 @@ +// Copyright © 2026 CCP ehf. +#include "uiCustomWidgets.h" + +#include +#include +#include +#include + +#include "uiConsts.h" + +// ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) +namespace ImGui +{ + +// threeway checkbox taken from https://github.com/ocornut/imgui/issues/2644 +bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ) +{ + bool ret; + if( *v_tristate == CheckBoxTriStateValue::MIXED ) + { + ImGui::PushItemFlag( ImGuiItemFlags_MixedValue, true ); + bool b = false; + ret = ImGui::Checkbox( label, &b ); + if( ret ) + { + *v_tristate = CheckBoxTriStateValue::CHECKED; + } + ImGui::PopItemFlag(); + } + else + { + bool b = ( *v_tristate != CheckBoxTriStateValue::UNCHECKED ); + ret = ImGui::Checkbox( label, &b ); + if( ret ) + { + *v_tristate = (CheckBoxTriStateValue)(int)b; + } + } + return ret; +} + +CheckBoxTriStateValue GetCheckedStatus( int64_t checked, int64_t count ) +{ + if( checked == 0 ) + { + return CheckBoxTriStateValue::UNCHECKED; + } + else if( checked == count ) + { + return CheckBoxTriStateValue::CHECKED; + } + else + { + return CheckBoxTriStateValue::MIXED; + } +} + + +bool FontAwesomeButton( const FaIcon& icon, int id, float width, float height ) +{ + const float faButtonPadding = 4.0f; + const float glyphWidth = UiConsts::FONT_AWESOME_SIZE * icon.xyRatio; + const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); + ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); + ImGui::PushID( id ); + bool ret = ImGui::Button( icon.text, ImVec2( width, height ) ); + ImGui::PopID(); + ImGui::PopStyleVar(); + + return ret; +} + +void FontAwesomeText( const FaIcon& icon, float width ) +{ + const float faButtonPadding = -4.0f; + const float glyphWidth = UiConsts::FONT_AWESOME_SIZE * icon.xyRatio; + const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); + ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); + ImGui::Text( icon.text ); + ImGui::PopStyleVar(); +} + +bool FontAwesomeSlashedButton( const FaIcon& icon, int id, float width, float height ) +{ + bool ret = ImGui::FontAwesomeButton( icon, id, width, height ); + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImGui::GetItemRectMax(); + float slashPadding = 1.5f; + ImGui::GetWindowDrawList()->AddLine( + ImVec2( min.x + slashPadding, max.y - slashPadding ), + ImVec2( max.x - slashPadding, min.y + slashPadding ), + ImGui::GetColorU32( ImGuiCol_Text ), + 2.0f ); + + return ret; +} + +const char* OpenCmfFileDialog() +{ + char const* filter[1] = { "*.cmf" }; + return tinyfd_openFileDialog( + "Open CMF File", + NULL, + 1, + filter, + "CMF Files", + 0 ); +} +} +// NOLINTEND(cppcoreguidelines-pro-type-vararg) \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiCustomWidgets.h b/src/viewer/rendering/ui/uiCustomWidgets.h new file mode 100644 index 0000000..79b8202 --- /dev/null +++ b/src/viewer/rendering/ui/uiCustomWidgets.h @@ -0,0 +1,62 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include +#include + +#include "uiConsts.h" + +namespace ImGui +{ +enum class CheckBoxTriStateValue +{ + UNCHECKED = 0, + CHECKED = 1, + MIXED = -1 +}; + +CheckBoxTriStateValue GetCheckedStatus( int64_t checked, int64_t count ); + +bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ); +bool FontAwesomeButton( const FaIcon& icon, int id = 0, float width = UiConsts::BUTTON_WIDTH, float height = UiConsts::BUTTON_HEIGHT ); +bool FontAwesomeSlashedButton( const FaIcon& icon, int id = 0, float width = UiConsts::BUTTON_WIDTH, float height = UiConsts::BUTTON_HEIGHT ); +void FontAwesomeText( const FaIcon& icon, float width ); +const char* OpenCmfFileDialog(); + +template +void ComboBox( const char* name, const std::vector& items, int32_t selectedIndex, Callable onChange ) +{ + if( items.empty() ) + { + if( ImGui::BeginCombo( name, "N/A" ) ) + { + ImGui::EndCombo(); + } + return; + } + + if( selectedIndex < 0 || selectedIndex >= static_cast( items.size() ) ) + { + selectedIndex = 0; + } + + if( ImGui::BeginCombo( name, items[selectedIndex].c_str() ) ) + { + for( int32_t itemIndex = 0; itemIndex < static_cast( items.size() ); ++itemIndex ) + { + const bool isSelected = ( itemIndex == selectedIndex ); + if( ImGui::Selectable( items[itemIndex].c_str(), isSelected ) ) + { + onChange( itemIndex ); + } + + if( isSelected ) + { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } +} +} \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp new file mode 100644 index 0000000..7fb3540 --- /dev/null +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -0,0 +1,698 @@ +// Copyright © 2026 CCP ehf. + +#include "uiGeneralWindow.h" + +#include +#include +#include +#include +#include +#include + +#include "uiCustomWidgets.h" + +// ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) + +namespace +{ +using AxisTriCheckboxStates = std::vector>; +using AxisCheckboxStates = std::vector>; + +/*** +* @brief Combines the states of a mesh state for a given axis into a single vector of checkbox states. +* @param meshState The mesh state to combine the axis states from. +* @param getAxisStates A callable that takes a MeshState and returns a StateCollection of pairs of uint32_t and bool representing the axis states. +*/ +template +AxisCheckboxStates GetAxisCheckboxStates( const MeshState& meshState, GetAxisStates getAxisStates ) +{ + AxisCheckboxStates combinedStates; + for( const auto& axis : getAxisStates( meshState ) ) + { + auto [index, checked] = axis.GetValue(); + combinedStates.push_back( { index, checked } ); + } + return combinedStates; +} + +/*** +* @brief Combines the states of multiple mesh states for a given axis into a single vector of tri-state checkbox states. +* @param meshStates The collection of mesh states to combine the axis states from. +* @param getAxisStates A callable that takes a MeshState and returns a StateCollection of pairs of uint32_t and bool representing the axis states. +*/ +template +AxisTriCheckboxStates GetAxisTriCheckboxStates( StateCollection& meshStates, GetAxisStates getAxisStates ) +{ + AxisTriCheckboxStates combinedStates; + + for( auto& meshState : meshStates ) + { + auto axisStatesForMesh = GetAxisCheckboxStates( meshState.GetValue(), getAxisStates ); + // combine the states + for( const auto& state : axisStatesForMesh ) + { + auto [index, checked] = state; + + auto foundState = std::find_if( combinedStates.begin(), combinedStates.end(), [&]( const auto& existingState ) { + return existingState.first == index; + } ); + auto stateAsTriState = checked ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::UNCHECKED; + if( foundState == combinedStates.end() ) + { + combinedStates.push_back( { index, stateAsTriState } ); + } + else if( foundState->second != stateAsTriState ) + { + foundState->second = ImGui::CheckBoxTriStateValue::MIXED; + } + } + } + + return combinedStates; +} + +/*** +* @brief Sets up a row of tri-state checkboxes for a model axis. +* @param checkboxStates The states of the checkboxes for the axis. +* @param name The name of the axis. +* @param changeCallback A callable that is invoked when a checkbox state changes. +*/ +template +void SetupModelAxisRow( AxisTriCheckboxStates& checkboxStates, const char* name, const Callable& changeCallback ) +{ + const int32_t maxIndex = checkboxStates.empty() ? -1 : static_cast( std::max_element( checkboxStates.begin(), checkboxStates.end(), []( const auto& a, const auto& b ) { + return a.first < b.first; + } )->first ); + + ImGui::BeginDisabled( maxIndex < 0 ); + ImGui::TableNextColumn(); + ImGui::Text( "%s", name ); + ImGui::SetItemTooltip( "Toggles the %s visualization for all meshes", name ); + ImGui::TableNextColumn(); + + if( maxIndex >= 0 ) + { + for( int32_t i = 0; i <= maxIndex; ++i ) + { + auto foundState = std::find_if( checkboxStates.begin(), checkboxStates.end(), [i]( const auto& state ) { + return state.first == static_cast( i ); + } ); + + if( foundState == checkboxStates.end() ) + { + ImGui::BeginDisabled( true ); + auto disabledValue = ImGui::CheckBoxTriStateValue::UNCHECKED; + ImGui::CheckBoxTristate( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &disabledValue ); + ImGui::SetItemTooltip( "No mesh has %s information for usage %d", name, i ); + ImGui::SameLine(); + ImGui::EndDisabled(); + } + else + { + auto value = foundState->second; + if( ImGui::CheckBoxTristate( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &value ) ) + { + changeCallback( value == ImGui::CheckBoxTriStateValue::CHECKED, static_cast( i ) ); + } + ImGui::SetItemTooltip( "Toggles debug visualization for %s with usage index %d for all meshes that have it", name, i ); + ImGui::SameLine(); + } + } + } + else + { + ImGui::BeginDisabled( true ); + bool value = false; + ImGui::Checkbox( ( std::string( "##" ) + name ).c_str(), &value ); + ImGui::SetItemTooltip( "Model doesn't have any %s", name ); + ImGui::SameLine(); + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); +} + +/*** +* @brief Sets up a row of checkboxes for a mesh axis. +* @param checkboxStates The states of the checkboxes for the axis. +* @param name The name of the axis. +* @param changeCallback A callable that is invoked when a checkbox state changes. +*/ +template +void SetupMeshAxisRow( AxisCheckboxStates& checkboxStates, const char* name, const Callable& changeCallback ) +{ + const int32_t maxIndex = checkboxStates.empty() ? -1 : static_cast( std::max_element( checkboxStates.begin(), checkboxStates.end(), []( const auto& a, const auto& b ) { + return a.first < b.first; + } )->first ); + + ImGui::BeginDisabled( maxIndex < 0 ); + ImGui::TableNextColumn(); + ImGui::Text( "%s", name ); + ImGui::SetItemTooltip( "Toggles the %s visualization for this mesh", name ); + ImGui::TableNextColumn(); + + if( maxIndex >= 0 ) + { + for( int32_t i = 0; i <= maxIndex; ++i ) + { + auto foundState = std::find_if( checkboxStates.begin(), checkboxStates.end(), [i]( const auto& state ) { + return state.first == static_cast( i ); + } ); + + if( foundState == checkboxStates.end() ) + { + ImGui::BeginDisabled( true ); + auto disabledValue = false; + ImGui::Checkbox( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &disabledValue ); + ImGui::SetItemTooltip( "No mesh has %s information for usage %d", name, i ); + ImGui::SameLine(); + ImGui::EndDisabled(); + } + else + { + auto value = foundState->second; + if( ImGui::Checkbox( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &value ) ) + { + changeCallback( value, static_cast( i ) ); + } + ImGui::SetItemTooltip( "Toggles debug visualization for %s with usage index %d for this mesh", name, i ); + ImGui::SameLine(); + } + } + } + else + { + ImGui::BeginDisabled( true ); + bool value = false; + ImGui::Checkbox( ( std::string( "##" ) + name ).c_str(), &value ); + ImGui::SetItemTooltip( "Mesh doesn't have any %s", name ); + ImGui::SameLine(); + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); +} + +template +void SetAxisStateForMeshes( StateCollection& meshStates, uint32_t index, bool checked, GetAxisStates getAxisStates ) +{ + for( auto& meshState : meshStates ) + { + auto& axisStates = getAxisStates( meshState.GetValue() ); + auto foundState = std::find_if( axisStates.begin(), axisStates.end(), [index]( const State>& state ) { + return state.GetValue().first == index; + } ); + if( foundState != axisStates.end() ) + { + foundState->SetValue( { index, checked } ); + } + } +} +} + +void UIGeneralWindow::Render( AppState& appState, float marginTop, float marginBottom ) +{ + const float width = (float)appState.windowSize.GetValue().first; + const float height = (float)appState.windowSize.GetValue().second; + + const float ySize = std::max( 1.0f, height - marginTop - marginBottom + 1 ); // +1 so we get an overlap of the borders + + ImGui::SetNextWindowPos( ImVec2( 0, marginTop ), ImGuiCond_Always ); + ImGui::SetNextWindowSizeConstraints( ImVec2( width / 16.0f, ySize ), ImVec2( width, ySize ) ); + ImGui::SetNextWindowSize( ImVec2( width / 4.0f, ySize ), ImGuiCond_FirstUseEver ); + ImGui::Begin( "CMF Info", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings ); + RenderGeneralInfo( appState ); + RenderMeshList( appState ); + RenderSkeletonList( appState ); + RenderAnimationOverrideList( appState ); + ImGui::End(); +} + +void UIGeneralWindow::RenderGeneralInfo( AppState& appState ) +{ + auto data = appState.cmfContent.GetValue(); + + // fetch the data + uint32_t vertexCount = 0; + uint32_t indexCount = 0; + size_t maxLod = 0; + std::vector lodNames{ "Auto" }; + if( data ) + { + const auto& cmfMeshes = data->m_cmfData->meshes; + const auto& modelStateMeshes = appState.modelState.meshes; + assert( cmfMeshes.size() == modelStateMeshes.size() ); + + for( uint32_t meshIndex = 0; meshIndex < cmfMeshes.size(); ++meshIndex ) + { + const auto& mesh = cmfMeshes[meshIndex]; + + auto& meshState = modelStateMeshes[meshIndex].GetValue(); + const auto& lod = mesh.lods[meshState.activeLod.GetValue()]; + + vertexCount += cmf::GetStreamElementCount( lod.vb ); + indexCount += cmf::GetStreamElementCount( lod.ib ); + maxLod = std::max( maxLod, mesh.lods.size() ); + } + + for( size_t i = 0; i < maxLod; ++i ) + { + lodNames.push_back( "Lod " + std::to_string( i ) ); + } + } + + ImGui::SeparatorText( "General" ); + if( ImGui::BeginTable( "##table", 2 ) ) + { + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); + + auto fileName = std::filesystem::path( appState.cmfPath.GetValue() ).filename().string(); + auto tooltip = std::string( "Full path: " ) + appState.cmfPath.GetValue(); + SetupAttribute( "Path", tooltip.c_str(), false, [&]() { ImGui::InputText( "##label", fileName.data(), fileName.size(), ImGuiInputTextFlags_ReadOnly ); } ); + + SetupAttribute( "Lods", "Number of LODs in the model", data == nullptr, [&]() { + ImGui::ComboBox( "##lod", lodNames, appState.modelState.selectedLod.GetValue() + 1, [&appState]( int32_t selectedLod ) { + appState.modelState.selectedLod.SetValue( selectedLod - 1 ); + } ); + } ); + SetupAttribute( "Vertices", "Vertex count of all meshes", false, [&]() { ImGui::Text( "%u", vertexCount ); } ); + SetupAttribute( "Indices", "Index count of all meshes", false, [&]() { ImGui::Text( "%u", indexCount ); } ); + SetupAttribute( "Meshes", "Number of meshes", false, [&]() { ImGui::Text( "%u", data ? data->m_cmfData->meshes.size() : 0 ); } ); + + SetupAttribute( "Visualization", "Visualization mode", data == nullptr, [&]() { + const auto& availableShaders = appState.modelState.availableShaders.GetValue(); + + // get the names + std::vector shaderNames; + shaderNames.reserve( availableShaders.size() ); + std::transform( availableShaders.begin(), availableShaders.end(), std::back_inserter( shaderNames ), []( const auto& pair ) { return pair.first; } ); + + // find the index of the active shader in the available shaders list + const auto& activeShader = appState.modelState.activeShader.GetValue(); + const auto& foundActiveShader = std::find_if( availableShaders.begin(), availableShaders.end(), [&activeShader]( const auto& pair ) { + return pair.first == activeShader.first && pair.second == activeShader.second; + } ); + + int32_t activeShaderIndex = static_cast( std::distance( availableShaders.begin(), foundActiveShader ) ); + + ImGui::ComboBox( "##visualization", shaderNames, activeShaderIndex, [&appState]( int32_t selectedIndex ) { + const auto& availableShaders = appState.modelState.availableShaders.GetValue(); + if( selectedIndex >= 0 && selectedIndex < static_cast( availableShaders.size() ) ) + { + appState.modelState.activeShader.SetValue( availableShaders[selectedIndex] ); + } + } ); + } ); + + SetupAttribute( "Display", "Toggle display of all meshes", data == nullptr, [&]() { + auto checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { + return state.GetValue().display.GetValue(); + } ); + ImGui::CheckBoxTriStateValue checkedState = ImGui::GetCheckedStatus( checkedCount, static_cast( appState.modelState.meshes.size() ) ); + if( ImGui::CheckBoxTristate( "##displaycheckbox", &checkedState ) ) + { + std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checkedState]( State& state ) { + state.GetValue().display.SetValue( checkedState == ImGui::CheckBoxTriStateValue::CHECKED ); + } ); + }; + } ); + + SetupAttribute( "Bounding Box", "Whole model bounding box", data == nullptr, [&] { + bool checked = appState.modelState.modelBoundingBox.GetValue(); + if( ImGui::Checkbox( "##boundingboxcheckbox", &checked ) ) + { + appState.modelState.modelBoundingBox.SetValue( checked ); + } + } ); + + SetupAttribute( "Wireframe Overlay", "Wireframe overlay for all meshes", data == nullptr, [&] { + auto checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { + return state.GetValue().wireframeOverlay.GetValue(); + } ); + ImGui::CheckBoxTriStateValue checkedState = ImGui::GetCheckedStatus( checkedCount, static_cast( appState.modelState.meshes.size() ) ); + if( ImGui::CheckBoxTristate( "##wireframecheckbox", &checkedState ) ) + { + std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checkedState]( State& state ) { + state.GetValue().wireframeOverlay.SetValue( checkedState == ImGui::CheckBoxTriStateValue::CHECKED ); + } ); + } + } ); + + bool hasAudioOcclusionMeshes = false; + if( data ) + { + hasAudioOcclusionMeshes = std::any_of( data->m_cmfData->meshes.begin(), data->m_cmfData->meshes.end(), []( const cmf::Mesh& mesh ) { + return !mesh.audioOcclusionMesh.vertices.empty(); + } ); + } + + SetupAttribute( "Audio Occlusion Mesh", "Audio occlusion mesh for all meshes", !hasAudioOcclusionMeshes, [&]() { + size_t checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { + return state.GetValue().audioOcclusionMesh.GetValue(); + } ); + ImGui::CheckBoxTriStateValue checkedState = ImGui::GetCheckedStatus( checkedCount, static_cast( appState.modelState.meshes.size() ) ); + if( ImGui::CheckBoxTristate( "##audioocclusioncheckbox", &checkedState ) ) + { + std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checkedState]( State& state ) { + state.GetValue().audioOcclusionMesh.SetValue( checkedState == ImGui::CheckBoxTriStateValue::CHECKED ); + } ); + } + } ); + + auto normalCheckboxes = GetAxisTriCheckboxStates( appState.modelState.meshes, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexNormals; + } ); + auto tangentCheckboxes = GetAxisTriCheckboxStates( appState.modelState.meshes, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexTangents; + } ); + auto binormalCheckboxes = GetAxisTriCheckboxStates( appState.modelState.meshes, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexBinormals; + } ); + + SetupModelAxisRow( normalCheckboxes, "Normals", [&appState]( bool checked, uint32_t index ) { + SetAxisStateForMeshes( appState.modelState.meshes, index, checked, []( MeshState& meshState ) -> auto& { + return meshState.showVertexNormals; + } ); + } ); + + SetupModelAxisRow( tangentCheckboxes, "Tangents", [&appState]( bool checked, uint32_t index ) { + SetAxisStateForMeshes( appState.modelState.meshes, index, checked, []( MeshState& meshState ) -> auto& { + return meshState.showVertexTangents; + } ); + } ); + + SetupModelAxisRow( binormalCheckboxes, "Binormals", [&appState]( bool checked, uint32_t index ) { + SetAxisStateForMeshes( appState.modelState.meshes, index, checked, []( MeshState& meshState ) -> auto& { + return meshState.showVertexBinormals; + } ); + } ); + + ImGui::EndTable(); + } +} + +void UIGeneralWindow::RenderMeshList( AppState& appState ) +{ + std::string header = "Meshes (" + std::to_string( appState.modelState.meshes.size() ) + ")"; + + ImGui::SeparatorText( header.c_str() ); + const auto data = appState.cmfContent.GetValue(); + size_t meshCount = data != nullptr && data->m_cmfData != nullptr ? data->m_cmfData->meshes.size() : 0; + + if( meshCount != appState.modelState.meshes.size() ) + { + return; + } + + for( int32_t index = 0; index < appState.modelState.meshes.size(); ++index ) + { + const auto& mesh = data->m_cmfData->meshes[index]; + auto& meshState = appState.modelState.meshes[index].GetValue(); + + RenderMeshInfo( mesh, meshState ); + } +} + +void UIGeneralWindow::RenderMeshInfo( const cmf::Mesh& mesh, MeshState& meshState ) +{ + if( ImGui::TreeNode( cmf::ToStdString( mesh.name ).c_str() ) ) + { + if( ImGui::BeginTable( "##table", 2 ) ) + { + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); + ImGui::TableNextRow(); + SetupAttribute( "Display", "Toggle display of this mesh", false, [&]() { + bool checked = meshState.display.GetValue(); + if( ImGui::Checkbox( "##displaycheckbox", &checked ) ) + { + meshState.display.SetValue( checked ); + } + } ); + SetupAttribute( "Bounding Box", "Toggles the bounding box of this mesh", false, [&]() { + bool checked = meshState.renderBoundingBox.GetValue(); + if( ImGui::Checkbox( "##boundingboxcheckbox", &checked ) ) + { + meshState.renderBoundingBox.SetValue( checked ); + } + } ); + SetupAttribute( "Wireframe Overlay", "Wireframe overlay for this mesh", false, [&]() { + bool checked = meshState.wireframeOverlay.GetValue(); + if( ImGui::Checkbox( "##wireframecheckbox", &checked ) ) + { + meshState.wireframeOverlay.SetValue( checked ); + } + } ); + + SetupAttribute( "Audio Occlusion Mesh", "Audio occlusion mesh for this mesh", mesh.audioOcclusionMesh.vertices.empty(), [&]() { + bool checked = meshState.audioOcclusionMesh.GetValue(); + if( ImGui::Checkbox( "##audioocclusioncheckbox", &checked ) ) + { + meshState.audioOcclusionMesh.SetValue( checked ); + } + } ); + + auto normalCheckboxes = GetAxisCheckboxStates( meshState, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexNormals; + } ); + auto tangentCheckboxes = GetAxisCheckboxStates( meshState, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexTangents; + } ); + auto binormalCheckboxes = GetAxisCheckboxStates( meshState, []( const MeshState& meshState ) -> auto& { + return meshState.showVertexBinormals; + } ); + + SetupMeshAxisRow( normalCheckboxes, "Normals", [&meshState]( bool checked, uint32_t index ) { + const auto& foundState = std::find_if( meshState.showVertexNormals.begin(), meshState.showVertexNormals.end(), [index]( const State>& state ) { + return state.GetValue().first == index; + } ); + + if( foundState != meshState.showVertexNormals.end() ) + { + foundState->SetValue( { index, checked } ); + } + } ); + + SetupMeshAxisRow( tangentCheckboxes, "Tangents", [&meshState]( bool checked, uint32_t index ) { + const auto& foundState = std::find_if( meshState.showVertexTangents.begin(), meshState.showVertexTangents.end(), [index]( const State>& state ) { + return state.GetValue().first == index; + } ); + + if( foundState != meshState.showVertexTangents.end() ) + { + foundState->SetValue( { index, checked } ); + } + } ); + + SetupMeshAxisRow( binormalCheckboxes, "Binormals", [&meshState]( bool checked, uint32_t index ) { + const auto& foundState = std::find_if( meshState.showVertexBinormals.begin(), meshState.showVertexBinormals.end(), [index]( const State>& state ) { + return state.GetValue().first == index; + } ); + + if( foundState != meshState.showVertexBinormals.end() ) + { + foundState->SetValue( { index, checked } ); + } + } ); + + ImGui::EndTable(); + RenderMorphList( mesh, meshState ); + } + ImGui::TreePop(); + } +} + +void UIGeneralWindow::RenderMorphList( const cmf::Mesh& mesh, MeshState& meshState ) +{ + if( ImGui::TreeNode( "##morphs", "Morphs (%zu)", mesh.morphTargets.targets.size() ) ) + { + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted( ICON_FA_MAGNIFYING_GLASS.text ); + ImGui::SameLine(); + m_morphFilter.Draw( "##morphfilter", ImGui::GetContentRegionAvail().x ); + ImGui::SetItemTooltip( "Filter morph targets" ); + + const auto& currentLod = mesh.lods[meshState.activeLod.GetValue()]; + for( size_t morphIndex = 0; morphIndex < currentLod.morphTargets.size(); ++morphIndex ) + { + const auto morphName = cmf::ToStdString( mesh.morphTargets.targets[morphIndex].name ); + if( !m_morphFilter.PassFilter( morphName.c_str() ) ) + { + continue; + } + + ImGui::SeparatorText( morphName.c_str() ); + + if( ImGui::BeginTable( "##table", 3 ) ) + { + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, UiConsts::BUTTON_WIDTH ); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + ImGui::Text( "Weight" ); + const auto& morph = currentLod.morphTargets[morphIndex]; + auto& morphState = meshState.morphs[morphIndex]; + + auto [weight, enabled] = morphState.GetValue(); + + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); + + if( ImGui::SliderFloat( ( std::string( "##morphslider" ) + std::to_string( morphIndex ) ).c_str(), &weight, 0.0f, 1.0f ) ) + { + morphState.SetValue( { weight, enabled } ); + } + ImGui::TableNextColumn(); + + if( ImGui::Checkbox( ( std::string( "##morphcheckbox" ) + std::to_string( morphIndex ) ).c_str(), &enabled ) ) + { + morphState.SetValue( { weight, enabled } ); + } + ImGui::EndTable(); + } + } + ImGui::TreePop(); + } +} + +void UIGeneralWindow::RenderSkeletonList( AppState& appState ) +{ + const auto& data = appState.cmfContent.GetValue(); + bool hasSkeletons = data && !data->m_cmfData->skeletons.empty(); + size_t skeletonCount = hasSkeletons ? data->m_cmfData->skeletons.size() : 0; + std::string header = "Skeletons (" + std::to_string( skeletonCount ) + ")"; + ImGui::SeparatorText( header.c_str() ); + + if( ImGui::BeginTable( "##table", 2 ) ) + { + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); + + SetupAttribute( "Display Joints", "Shows skeleton joints", !hasSkeletons, [&]() { + bool checked = appState.modelState.jointDebug.GetValue(); + if( ImGui::Checkbox( "##displayjoints", &checked ) ) + { + appState.modelState.jointDebug.SetValue( checked ); + } + } ); + + SetupAttribute( "Display Joint Axes", "Shows skeleton joint axes", !hasSkeletons, [&]() { + bool checked = appState.modelState.jointAxisDebug.GetValue(); + if( ImGui::Checkbox( "##displayjointaxes", &checked ) ) + { + appState.modelState.jointAxisDebug.SetValue( checked ); + } + } ); + + SetupAttribute( "Display Bones", "Shows skeleton bones", !hasSkeletons, [&]() { + bool checked = appState.modelState.boneDebug.GetValue(); + if( ImGui::Checkbox( "##displaybones", &checked ) ) + { + appState.modelState.boneDebug.SetValue( checked ); + } + } ); + ImGui::EndTable(); + } +} + +void UIGeneralWindow::RenderAnimationOverrideList( AppState& appState ) +{ + ImGui::SeparatorText( "Animation Owners" ); + + const auto& data = appState.cmfContent.GetValue(); + bool disabled = data == nullptr || data->m_cmfData->skeletons.empty(); + ImGui::BeginDisabled( disabled ); + + // button to add an animation owner from a cmf file + if( ImGui::Button( "+", ImVec2( ImGui::GetContentRegionAvail().x, UiConsts::BUTTON_HEIGHT ) ) ) + { + auto* path = ImGui::OpenCmfFileDialog(); + if( path != nullptr ) + { + auto data = CmfContentLoader::LoadContentFromFile( path ); + if( data ) + { + appState.modelState.animationOverrides.AddState( data ); + appState.modelState.activeAnimationOwner.SetValue( data ); + } + } + } + if( data == nullptr ) + { + ImGui::SetItemTooltip( "Adds an animation owner from a cmf file. Disabled because no data is loaded." ); + } + else if( data->m_cmfData->skeletons.empty() ) + { + ImGui::SetItemTooltip( "Adds an animation owner from a cmf file. Disabled since model has no skeletons." ); + } + else + { + ImGui::SetItemTooltip( "Adds an animation owner from a cmf file." ); + } + + std::vector> allModels{}; + if( data ) + { + allModels.reserve( appState.modelState.animationOverrides.size() + 1 ); + allModels.push_back( data ); + } + std::transform( appState.modelState.animationOverrides.begin(), appState.modelState.animationOverrides.end(), std::back_inserter( allModels ), []( const auto& state ) { return state.GetValue(); } ); + + const auto& skeletonOwners = appState.modelState.animationOverrides; + + uint32_t index = 0; + for( auto& animationOwner : allModels ) + { + auto name = std::filesystem::path( animationOwner->m_filePath ).filename().string(); + if( index == 0 ) + { + name += " (model)"; + } + + if( ImGui::RadioButton( name.c_str(), appState.modelState.activeAnimationOwner.GetValue() == animationOwner ) ) + { + appState.modelState.activeAnimationOwner.SetValue( animationOwner ); + } + + if( ImGui::BeginItemTooltip() ) + { + if( index == 0 ) + { + ImGui::Text( "Model skeleton and animations" ); + } + else + { + ImGui::Text( "Animation owner from %s", animationOwner->m_filePath.c_str() ); + } + ImGui::Text( "Skeletons" ); + for( const auto& skeleton : animationOwner->m_cmfData->skeletons ) + { + ImGui::BulletText( "%s has %d bones", cmf::ToStdString( skeleton.name ), skeleton.bones.size() ); + } + ImGui::EndTooltip(); + } + + if( index != 0 ) + { + ImGui::SameLine( ImGui::GetContentRegionAvail().x - UiConsts::BUTTON_WIDTH ); + std::string label = "-##" + std::to_string( index ); + if( ImGui::Button( label.c_str() ) ) + { + appState.modelState.animationOverrides.RemoveAt( index - 1 ); + + if( appState.modelState.activeAnimationOwner.GetValue() == animationOwner ) + { + appState.modelState.activeAnimationOwner.SetValue( appState.cmfContent.GetValue() ); + } + } + ImGui::SetItemTooltip( "Removes %s ", name.c_str() ); + } + ++index; + } + ImGui::EndDisabled(); +} + + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiGeneralWindow.h b/src/viewer/rendering/ui/uiGeneralWindow.h new file mode 100644 index 0000000..1eb588e --- /dev/null +++ b/src/viewer/rendering/ui/uiGeneralWindow.h @@ -0,0 +1,47 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "../../appState.h" +#include +#include +#include +#include + +// ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) +class UIGeneralWindow +{ +public: + void Render( AppState& appState, float marginTop, float marginBottom ); + +private: + void RenderGeneralInfo( AppState& appState ); + void RenderMeshList( AppState& appState ); + void RenderMeshInfo( const cmf::Mesh& mesh, MeshState& meshState ); + void RenderMorphList( const cmf::Mesh& mesh, MeshState& meshState ); + void RenderSkeletonList( AppState& appState ); + void RenderAnimationOverrideList( AppState& appState ); + + ImGuiTextFilter m_morphFilter; + + template + void SetupAttribute( const char* name, const char* tooltip, bool disabled, Callable constructor ) + { + ImGui::TableNextRow(); + + ImGui::BeginDisabled( disabled ); + ImGui::TableNextColumn(); + + ImGui::TextUnformatted( name ); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); + constructor(); + if( tooltip ) + { + ImGui::SetItemTooltip( tooltip ); + } + ImGui::EndDisabled(); + } +}; +// NOLINTEND(cppcoreguidelines-pro-type-vararg) \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiMenubar.cpp b/src/viewer/rendering/ui/uiMenubar.cpp new file mode 100644 index 0000000..d574d07 --- /dev/null +++ b/src/viewer/rendering/ui/uiMenubar.cpp @@ -0,0 +1,124 @@ +// Copyright © 2026 CCP ehf. + +#include "uiMenubar.h" +#include + +#include "uiCustomWidgets.h" + +void UiMenubar::Render( AppState& appState, MenuState& menuState ) +{ + if( ImGui::BeginMainMenuBar() ) + { + if( ImGui::BeginMenu( "File" ) ) + { + if( ImGui::MenuItem( "Open", "Ctrl+O" ) ) + { + auto filePath = ImGui::OpenCmfFileDialog(); + if( filePath != nullptr ) + { + appState.cmfLoadRequest.SetValue( filePath ); + } + } + if( appState.cmfContent.GetValue() == nullptr ) + { + ImGui::BeginDisabled(); + } + if( ImGui::MenuItem( "Load Animation Override" ) ) + { + const auto* filePath = ImGui::OpenCmfFileDialog(); + if( filePath != nullptr ) + { + auto data = CmfContentLoader::LoadContentFromFile( filePath ); + if( data ) + { + appState.modelState.animationOverrides.AddState( data ); + } + } + } + if( appState.cmfContent.GetValue() == nullptr ) + { + ImGui::EndDisabled(); + } + + ImGui::Separator(); + if( ImGui::MenuItem( "Exit" ) ) + { + appState.exitRequested.SetValue( true ); + } + ImGui::EndMenu(); + } + auto content = appState.cmfContent.GetValue(); + + if( ImGui::BeginMenu( "View" ) ) + { + if( ImGui::BeginMenu( "Camera", content != nullptr ) ) + { + std::vector> focusObjects; + if( content != nullptr ) + { + const auto& meshes = content->m_cmfData->meshes; + + if( meshes.size() > 1 ) + { + focusObjects.push_back( { "Whole Model", content->GetBoundingSphere() } ); + } + std::for_each( meshes.begin(), meshes.end(), [&focusObjects]( const cmf::Mesh& mesh ) { + focusObjects.push_back( { cmf::ToStdString( mesh.name ), CcpMath::Sphere( mesh.bounds ) } ); + } ); + } + + uint32_t index = 0; + for( const auto& [menuName, sphere] : focusObjects ) + { + std::string menuLabel = "Focus on " + menuName; + const char* shortcut = nullptr; + if( index == 0 ) + { + shortcut = "Ctrl+F"; + } + if( ImGui::MenuItem( menuLabel.c_str(), shortcut ) ) + { + appState.cameraFocus.ForceSetValue( sphere ); + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); + } + ++index; + } + ImGui::Separator(); + if( ImGui::MenuItem( "Look Right (+X)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_RIGHT ); + } + if( ImGui::MenuItem( "Look Left (-X)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_LEFT ); + } + if( ImGui::MenuItem( "Look Up (+Y)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_UP ); + } + if( ImGui::MenuItem( "Look Down (-Y)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_DOWN ); + } + if( ImGui::MenuItem( "Look Front (-Z)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_FRONT ); + } + if( ImGui::MenuItem( "Look Back (+Z)" ) ) + { + appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_BACK ); + } + ImGui::EndMenu(); + } + ImGui::Separator(); + const char* toggleUiLabel = menuState.showUi ? "Hide UI" : "Show UI"; + if( ImGui::MenuItem( toggleUiLabel, "Ctrl+F12" ) ) + { + menuState.showUi = !menuState.showUi; + } + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } +} diff --git a/src/viewer/rendering/ui/uiMenubar.h b/src/viewer/rendering/ui/uiMenubar.h new file mode 100644 index 0000000..31f138b --- /dev/null +++ b/src/viewer/rendering/ui/uiMenubar.h @@ -0,0 +1,16 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "../../appState.h" + +struct MenuState +{ + bool showUi{ true }; +}; + +class UiMenubar +{ +public: + void Render( AppState& appState, MenuState& menuState ); +}; \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index bf206af..25a7362 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -3,91 +3,16 @@ #include "uiRenderer.h" #include -#include -#include -#include #include #include -#include -#include - #include "appState.h" #include "../vulkan/vulkanerrors.h" - -#include #include -const float MENU_BAR_HEIGHT = 18.0f; -const float ANIMATION_PLAYER_HEIGHT = 36.0f; -const float BUTTON_WIDTH = 19.0f; -const float BUTTON_HEIGHT = 19.0f; -const ImVec2 BUTTON_SIZE = ImVec2( BUTTON_WIDTH, BUTTON_HEIGHT ); -const float FONT_AWESOME_SIZE = 13.0f; - // ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file // NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) -namespace ImGui -{ - -// threeway checkbox taken from https://github.com/ocornut/imgui/issues/2644 -bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ) -{ - bool ret; - if( *v_tristate == CheckBoxTriStateValue::MIXED ) - { - ImGui::PushItemFlag( ImGuiItemFlags_MixedValue, true ); - bool b = false; - ret = ImGui::Checkbox( label, &b ); - if( ret ) - { - *v_tristate = CheckBoxTriStateValue::CHECKED; - } - ImGui::PopItemFlag(); - } - else - { - bool b = ( *v_tristate != CheckBoxTriStateValue::UNCHECKED ); - ret = ImGui::Checkbox( label, &b ); - if( ret ) - { - *v_tristate = (CheckBoxTriStateValue)(int)b; - } - } - return ret; -} - -bool FontAwesomeButton( const FaIcon& icon, int id = 0, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) -{ - const float faButtonPadding = 4.0f; - const float glyphWidth = FONT_AWESOME_SIZE * icon.xyRatio; - const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); - ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); - ImGui::PushID( id ); - bool ret = ImGui::Button( icon.text, ImVec2( width, height ) ); - ImGui::PopID(); - ImGui::PopStyleVar(); - - return ret; -} - -bool FontAwesomeSlashedButton( const FaIcon& icon, int id = 0, float width = BUTTON_WIDTH, float height = BUTTON_HEIGHT ) -{ - bool ret = ImGui::FontAwesomeButton( icon, id, width, height ); - ImVec2 min = ImGui::GetItemRectMin(); - ImVec2 max = ImGui::GetItemRectMax(); - float slashPadding = 1.5f; - ImGui::GetWindowDrawList()->AddLine( - ImVec2( min.x + slashPadding, max.y - slashPadding ), - ImVec2( max.x - slashPadding, min.y + slashPadding ), - ImGui::GetColorU32( ImGuiCol_Text ), - 2.0f ); - - return ret; -} - -} UIRenderer::UIRenderer( std::shared_ptr renderer ) : m_renderer( renderer ), @@ -129,7 +54,7 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) ImFontConfig config; config.MergeMode = true; // add fontawesome icons - io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, fa_solid_900_ttf_size, FONT_AWESOME_SIZE, &config, iconRanges ); + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, fa_solid_900_ttf_size, UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); // Setup Dear ImGui style ImGui::StyleColorsDark(); @@ -197,57 +122,12 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) { m_loadStatus = LoadStatus::SUCCESSFUL; } - RegisterModelCallbacks( appState ); } ); auto [width, height] = state.windowSize.GetValue(); m_graphicsCommandBuffer.SetRenderSize( width, height ); - UpdateUiState( state ); -} - -void UIRenderer::RegisterModelCallbacks( AppState& appState ) -{ - // Register callbacks for model state changes - appState.modelState.currentAnimation.RegisterCallback( [this]( std::string animationName, AppState& appState ) { - m_playback.currentTime = 0.0f; - m_playback.playing = false; - - auto animationOwner = appState.modelState.activeAnimationOwner.GetValue(); - if( !animationName.empty() && animationOwner != nullptr ) - { - for( const auto& animation : animationOwner->m_cmfData->animations ) - { - if( cmf::ToStdString( animation.name ) == animationName ) - { - m_playback.duration = animation.duration; - return; - } - } - } - else - { - m_playback.duration = 0.0f; - } - } ); - - appState.modelState.activeAnimationOwner.RegisterCallback( [this]( std::shared_ptr activeAnimationOwner, AppState& appState ) { - appState.modelState.currentAnimation.SetValue( "" ); - appState.modelState.currentAnimationTime.SetValue( 0.0f ); - m_playback = Playback{}; - } ); } -const char* UIRenderer::FileOpenDialog() -{ - char const* filter[1] = { "*.cmf" }; - return tinyfd_openFileDialog( - "Open CMF File", - NULL, - 1, - filter, - "CMF Files", - 0 ); -} void UIRenderer::BeginFrame() { @@ -259,7 +139,6 @@ void UIRenderer::BeginFrame() void UIRenderer::Render( AppState& appState ) { - SetupMenubar( appState ); SetupUi( appState ); m_graphicsCommandBuffer.Begin( m_renderer.get() ); ImGui::Render(); @@ -270,810 +149,16 @@ void UIRenderer::Render( AppState& appState ) void UIRenderer::SetupUi( AppState& appState ) { - UpdateUiState( appState ); - if( m_showMainUI ) + m_menubar.Render( appState, m_menuState ); + if( m_menuState.showUi ) { - CMFInfoWindow( appState ); - m_detailWindow.Render( appState, MENU_BAR_HEIGHT, ANIMATION_PLAYER_HEIGHT ); - - SetupPlaybackControls( appState ); + m_generalWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); + m_detailWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); + m_animationPlayback.Render( appState ); } SetupPopupWindows( appState ); } -void UIRenderer::CMFInfoWindow( AppState& appState ) -{ - bool open = true; - - float width = (float)appState.windowSize.GetValue().first; - float height = (float)appState.windowSize.GetValue().second; - - float ySize = std::max( 1.0f, height - MENU_BAR_HEIGHT - ANIMATION_PLAYER_HEIGHT + 1 ); // +1 so we get an overlap of the borders - - ImGui::SetNextWindowPos( ImVec2( 0, 18 ), ImGuiCond_Always ); - ImGui::SetNextWindowSizeConstraints( ImVec2( 0, ySize ), ImVec2( width, ySize ) ); - ImGui::SetNextWindowSize( ImVec2( width / 4.0f, ySize ), ImGuiCond_FirstUseEver ); - if( ImGui::Begin( "CMF Info", &open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings ) ) - { - SetupGeneralView( appState ); - - SetupMeshListView( m_uiState.modelStates, appState ); - - SetupSkeletonOwners( m_uiState.skeletonOwners, appState ); - } - ImGui::End(); -} - -void UIRenderer::SetupGeneralView( AppState& appState ) -{ - ImGui::SeparatorText( "General" ); - if( ImGui::BeginTable( "##table", 2 ) ) - { - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Path" ); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); - ImGui::InputText( "##label", m_uiState.filePath.data(), m_uiState.filePath.size(), ImGuiInputTextFlags_ReadOnly ); - ImGui::SetItemTooltip( "%s", m_uiState.filePath.data() ); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Lod" ); - ImGui::TableNextColumn(); - SetupCombo( "##lod", m_uiState.modelStates.lod, appState.modelState.selectedLod ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Vertices" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u", m_uiState.modelStates.vertexCount ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Indices" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u", m_uiState.modelStates.indexCount ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Meshes" ); - ImGui::TableNextColumn(); - ImGui::Text( "%zu", m_uiState.modelStates.meshes.size() ); - ImGui::TableNextRow(); - ImGui::BeginDisabled( appState.modelState.meshes.empty() ); - - ImGui::TableNextColumn(); - ImGui::Text( "Visualization" ); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); - SetupCombo( "##visualiationMode", m_uiState.visualizationShaderComboBox, appState.modelState.activeShader ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Display" ); - ImGui::TableNextColumn(); - size_t checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { - return state.GetValue().display.GetValue(); - } ); - ImGui::CheckBoxTriStateValue checked = ( checkedCount == 0 ) ? ImGui::CheckBoxTriStateValue::UNCHECKED : ( checkedCount == appState.modelState.meshes.size() ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::MIXED ); - OnChange( ImGui::CheckBoxTristate( "##displaycheckbox", &checked ), [&appState, &checked]() { - std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checked]( State& state ) { - state.GetValue().display.SetValue( checked == ImGui::CheckBoxTriStateValue::CHECKED ); - } ); - } ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Bounding Box" ); - ImGui::TableNextColumn(); - bool boundingBox = m_uiState.modelStates.boundingBox; - OnChange( ImGui::Checkbox( "##boundingboxcheckbox", &boundingBox ), [&appState, &boundingBox]() { - appState.modelState.modelBoundingBox.SetValue( boundingBox ); - } ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Wireframe Overlay" ); - ImGui::TableNextColumn(); - checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { - return state.GetValue().wireframeOverlay.GetValue(); - } ); - checked = ( checkedCount == 0 ) ? ImGui::CheckBoxTriStateValue::UNCHECKED : ( checkedCount == appState.modelState.meshes.size() ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::MIXED ); - OnChange( ImGui::CheckBoxTristate( "##wireframecheckbox", &checked ), [&appState, &checked]() { - std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checked]( State& state ) { - state.GetValue().wireframeOverlay.SetValue( checked == ImGui::CheckBoxTriStateValue::CHECKED ); - } ); - } ); - ImGui::TableNextRow(); - ImGui::EndDisabled(); - - bool hasAudioOcclusionMeshes = std::any_of( m_uiState.modelStates.meshes.begin(), m_uiState.modelStates.meshes.end(), []( const MeshUiState& state ) { - return state.hasAudioOcclusionMesh; - } ); - ImGui::BeginDisabled( !hasAudioOcclusionMeshes ); - ImGui::TableNextColumn(); - ImGui::Text( "Audio Occlusion Mesh" ); - ImGui::TableNextColumn(); - checkedCount = std::count_if( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), []( const State& state ) { - return state.GetValue().audioOcclusionMesh.GetValue(); - } ); - checked = ( checkedCount == 0 ) ? ImGui::CheckBoxTriStateValue::UNCHECKED : ( checkedCount == appState.modelState.meshes.size() ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::MIXED ); - OnChange( ImGui::CheckBoxTristate( "##audioocclusioncheckbox", &checked ), [&appState, &checked]() { - std::for_each( appState.modelState.meshes.begin(), appState.modelState.meshes.end(), [checked]( State& state ) { - state.GetValue().audioOcclusionMesh.SetValue( checked == ImGui::CheckBoxTriStateValue::CHECKED ); - } ); - } ); - ImGui::TableNextRow(); - ImGui::EndDisabled(); - - SetupModelAxisRows( appState ); - - ImGui::EndTable(); - } -} - -void UIRenderer::SetupMeshListView( const ModelUiState& modelState, AppState& appState ) -{ - bool open = true; - - if( !modelState.meshes.empty() ) - { - std::string header = "Meshes (" + std::to_string( modelState.meshes.size() ) + ")"; - if( ImGui::CollapsingHeader( header.c_str(), &open, ImGuiTreeNodeFlags_DefaultOpen ) ) - { - for( const auto& mesh : modelState.meshes ) - { - SetupMeshView( mesh, appState ); - } - } - } -} - -void UIRenderer::SetupModelAxisRows( AppState& appState ) -{ - std::vector>> allNormalStates; - std::vector>> allTangentStates; - std::vector>> allBinormalStates; - - for( const auto& meshState : appState.modelState.meshes ) - { - allNormalStates.push_back( meshState.GetValue().showVertexNormals ); - allTangentStates.push_back( meshState.GetValue().showVertexTangents ); - allBinormalStates.push_back( meshState.GetValue().showVertexBinormals ); - } - - auto normalCheckboxes = GetAxisTriCheckboxStates( allNormalStates ); - auto tangentCheckboxes = GetAxisTriCheckboxStates( allTangentStates ); - auto binormalCheckboxes = GetAxisTriCheckboxStates( allBinormalStates ); - - SetupModelAxisRow( normalCheckboxes, std::string( "Normals" ), [&appState]( bool checked, uint32_t index ) { - for( auto& meshState : appState.modelState.meshes ) - { - auto& vertexNormalStates = meshState.GetValue().showVertexNormals; - auto foundState = std::find_if( vertexNormalStates.begin(), vertexNormalStates.end(), [index]( const State>& state ) { - return state.GetValue().first == index; - } ); - if( foundState != vertexNormalStates.end() ) - { - foundState->SetValue( { index, checked } ); - } - } - } ); - - SetupModelAxisRow( tangentCheckboxes, std::string( "Tangents" ), [&appState]( bool checked, uint32_t index ) { - for( auto& meshState : appState.modelState.meshes ) - { - auto& vertexTangentStates = meshState.GetValue().showVertexTangents; - auto foundState = std::find_if( vertexTangentStates.begin(), vertexTangentStates.end(), [index]( const State>& state ) { - return state.GetValue().first == index; - } ); - if( foundState != vertexTangentStates.end() ) - { - foundState->SetValue( { index, checked } ); - } - } - } ); - - SetupModelAxisRow( binormalCheckboxes, std::string( "Bitangents" ), [&appState]( bool checked, uint32_t index ) { - for( auto& meshState : appState.modelState.meshes ) - { - auto& vertexBinormalStates = meshState.GetValue().showVertexBinormals; - auto foundState = std::find_if( vertexBinormalStates.begin(), vertexBinormalStates.end(), [index]( const State>& state ) { - return state.GetValue().first == index; - } ); - if( foundState != vertexBinormalStates.end() ) - { - foundState->SetValue( { index, checked } ); - } - } - } ); -} - -std::vector> UIRenderer::GetAxisTriCheckboxStates( const std::vector>>& axisStates ) -{ - std::vector> combinedStates; - - for( const auto& meshState : axisStates ) - { - for( const auto& axis : meshState ) - { - auto [index, checked] = axis.GetValue(); - - auto foundState = std::find_if( combinedStates.begin(), combinedStates.end(), [idx = index]( const std::pair& pair ) { - return pair.first == idx; - } ); - - if( foundState == combinedStates.end() ) - { - combinedStates.push_back( { index, checked ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::UNCHECKED } ); - } - else - { - if( foundState->second != ImGui::CheckBoxTriStateValue::MIXED ) - { - if( ( foundState->second == ImGui::CheckBoxTriStateValue::CHECKED && !checked ) || ( foundState->second == ImGui::CheckBoxTriStateValue::UNCHECKED && checked ) ) - { - foundState->second = ImGui::CheckBoxTriStateValue::MIXED; - } - } - } - } - } - - return combinedStates; -} - -void UIRenderer::SetupVertexAxisRows( MeshState& meshAppState ) -{ - auto usageIndexChecker = []( const State>& pair1, const State>& pair2 ) { - return pair1.GetValue().first < pair2.GetValue().first; - }; - - auto maxNormalIndexState = std::max_element( meshAppState.showVertexNormals.begin(), meshAppState.showVertexNormals.end(), usageIndexChecker ); - auto maxTangentIndexState = std::max_element( meshAppState.showVertexTangents.begin(), meshAppState.showVertexTangents.end(), usageIndexChecker ); - auto maxBitangentIndexState = std::max_element( meshAppState.showVertexBinormals.begin(), meshAppState.showVertexBinormals.end(), usageIndexChecker ); - - int32_t maxNormalIndex = maxNormalIndexState != meshAppState.showVertexNormals.end() ? maxNormalIndexState->GetValue().first : -1; - int32_t maxTangentIndex = maxTangentIndexState != meshAppState.showVertexTangents.end() ? maxTangentIndexState->GetValue().first : -1; - int32_t maxBitangentIndex = maxBitangentIndexState != meshAppState.showVertexBinormals.end() ? maxBitangentIndexState->GetValue().first : -1; - - SetupVertexAxisRow( meshAppState.showVertexNormals, "Normals", maxNormalIndex ); - SetupVertexAxisRow( meshAppState.showVertexTangents, "Tangents", maxTangentIndex ); - SetupVertexAxisRow( meshAppState.showVertexBinormals, "Bitangents", maxBitangentIndex ); -} - -void UIRenderer::SetupVertexAxisRow( StateCollection>& vertexAxisStates, const char* label, int maxUsageIndex ) -{ - ImGui::BeginDisabled( vertexAxisStates.empty() ); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::TextUnformatted( label ); - ImGui::TableNextColumn(); - - if( maxUsageIndex >= 0 ) - { - for( int32_t i = 0; i <= maxUsageIndex; ++i ) - { - // find the states that have the index of the column (f.ex there is nothing stopping a mesh having one color attribute that is using usageindex at 16) - auto foundElement = std::find_if( vertexAxisStates.begin(), vertexAxisStates.end(), [i]( const State>& state ) { - return state.GetValue().first == i; - } ); - - if( foundElement != vertexAxisStates.end() ) - { - bool changed = ImGui::Checkbox( ( std::string( "##" ) + label + std::to_string( i ) ).c_str(), &foundElement->GetValue().second ); - - OnChange( changed, [foundElement]() { - foundElement->SetValue( { foundElement->GetValue().first, foundElement->GetValue().second } ); - } ); - - ImGui::SetItemTooltip( "Toggles debug visualization for %s with usage index %d", label, i ); - ImGui::SameLine(); - } - else - { - ImGui::BeginDisabled( true ); - bool value = false; - ImGui::Checkbox( ( std::string( "##" ) + label + std::to_string( i ) ).c_str(), &value ); - ImGui::SetItemTooltip( "Mesh doesn't have %s with usage index %d.", label, i ); - ImGui::SameLine(); - ImGui::EndDisabled(); - } - } - ImGui::NewLine(); - } - else - { - ImGui::BeginDisabled( true ); - bool value = false; - ImGui::Checkbox( ( std::string( "##" ) + label ).c_str(), &value ); - ImGui::SetItemTooltip( "Mesh doesn't have %s", label ); - ImGui::SameLine(); - ImGui::EndDisabled(); - } - ImGui::EndDisabled(); -} - -void UIRenderer::SetupMeshView( const MeshUiState& mesh, AppState& appState ) -{ - if( ImGui::TreeNode( mesh.name.c_str() ) ) - { - auto& meshAppState = appState.modelState.meshes[mesh.meshIndex].GetValue(); - if( ImGui::BeginTable( "##table", 2 ) ) - { - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Vertex Count" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u", mesh.vertexCount ); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Index Count" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u", mesh.indexCount ); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Current Lod" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u/%u", mesh.lod, mesh.maxLodIndex ); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Screen Size" ); - ImGui::SetItemTooltip( "The approximate screen size based on the generated bounding sphere with radius %.3f (if it is infinite then the camera is inside of the sphere)", mesh.boundingSphereRadius ); - ImGui::TableNextColumn(); - if( mesh.screenSize == std::numeric_limits::max() ) - { - ImGui::Text( "infinite" ); - } - else - { - ImGui::Text( "%upx", uint32_t( mesh.screenSize ) ); - } - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Display" ); - ImGui::SetItemTooltip( "Toggles the \"%s\" mesh", mesh.name.c_str() ); - - ImGui::TableNextColumn(); - bool display = mesh.display; - OnChange( ImGui::Checkbox( "##displaycheckbox", &display ), [&meshAppState, &display]() { - meshAppState.display.SetValue( display ); - } ); - - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Bounding Box" ); - ImGui::SetItemTooltip( "Toggles the bounding box for \"%s\" mesh", mesh.name.c_str() ); - - ImGui::TableNextColumn(); - bool boundingBox = mesh.boundingBox; - OnChange( ImGui::Checkbox( "##boundingboxcheckbox", &boundingBox ), [&meshAppState, &boundingBox]() { - meshAppState.renderBoundingBox.SetValue( boundingBox ); - } ); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - ImGui::Text( "Wireframe Overlay" ); - ImGui::SetItemTooltip( "Toggles the wireframe overlay for the \"%s\" mesh", mesh.name.c_str() ); - - ImGui::TableNextColumn(); - bool wireframeOverlay = mesh.wireframeOverlay; - OnChange( ImGui::Checkbox( "##wireframeoverlaycheckbox", &wireframeOverlay ), [&meshAppState, &wireframeOverlay]() { - meshAppState.wireframeOverlay.SetValue( wireframeOverlay ); - } ); - ImGui::TableNextRow(); - - ImGui::BeginDisabled( !mesh.hasAudioOcclusionMesh ); - ImGui::TableNextColumn(); - ImGui::Text( "Audio Occlusion Mesh" ); - ImGui::SetItemTooltip( "Toggles the audio occlusion mesh rendering for the \"%s\" mesh", mesh.name.c_str() ); - ImGui::TableNextColumn(); - bool audioOcclusion = mesh.audioOcclusionMesh; - OnChange( ImGui::Checkbox( "##audioocclusionmeshcheckbox", &audioOcclusion ), [&meshAppState, &audioOcclusion]() { - meshAppState.audioOcclusionMesh.SetValue( audioOcclusion ); - } ); - ImGui::EndDisabled(); - - SetupVertexAxisRows( meshAppState ); - - ImGui::EndTable(); - - std::string header = "Morph Targets (" + std::to_string( mesh.morphTargets.size() ) + ")"; - if( ImGui::CollapsingHeader( header.c_str() ) ) - { - for( const auto& morphTarget : mesh.morphTargets ) - { - SetupMorphTarget( morphTarget, mesh.meshIndex, appState ); - } - } - } - ImGui::TreePop(); - } -} - -void UIRenderer::SetupMorphTarget( const MorphTargetUiState& morphTarget, size_t meshIndex, AppState& appState ) -{ - ImGui::SeparatorText( morphTarget.name.c_str() ); - - if( ImGui::BeginTable( "##table", 3 ) ) - { - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, BUTTON_WIDTH ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Weight" ); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); - float weight = morphTarget.weight; - OnChange( ImGui::SliderFloat( "##slider", &weight, 0.0f, 1.0f, "%.6f" ), [&appState, &morphTarget, &weight, meshIndex]() { - auto& morphState = appState.modelState.meshes[meshIndex].GetValue().morphs[morphTarget.index]; - morphState.SetValue( { weight, morphState.GetValue().second } ); - } ); - ImGui::TableNextColumn(); - bool enabled = morphTarget.enabled; - - OnChange( ImGui::Checkbox( "##checkbox", &enabled ), [&appState, &morphTarget, &enabled, meshIndex]() { - auto& morphState = appState.modelState.meshes[meshIndex].GetValue().morphs[morphTarget.index]; - morphState.SetValue( { morphState.GetValue().first, enabled } ); - } ); - ImGui::SetItemTooltip( "Toggles the \"%s\" morph target", morphTarget.name.c_str() ); - ImGui::EndTable(); - } -} - -void UIRenderer::SetupSkeletonOwners( const std::vector& skeletonOwners, AppState& appState ) -{ - // static meshes - if( skeletonOwners.empty() ) - { - std::string header = "Skeletons ( 0 )"; - if( ImGui::CollapsingHeader( header.c_str(), ImGuiTreeNodeFlags_None ) ) - { - ImGui::BeginDisabled(); - ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, 0, ImGui::GetContentRegionAvail().x ); - ImGui::SetItemTooltip( "Adds an animation override - Disabled since model doesn't have a skeleton" ); - ImGui::EndDisabled(); - } - return; - } - - // The first skeleton owner is the model, the rest are overrides - std::string header = "Skeletons ( " + std::to_string( skeletonOwners.size() ) + " )"; - if( ImGui::CollapsingHeader( header.c_str(), ImGuiTreeNodeFlags_DefaultOpen ) ) - { - // debug options show bones, joints and joint axis - ImGui::BeginTable( "##SkeletonDebug", 2 ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - - ImGui::Text( "Skeleton Joint" ); - ImGui::TableNextColumn(); - OnChange( ImGui::Checkbox( "##skeletonjoint", &m_uiState.jointDebug ), [&appState, this]() { - appState.modelState.jointDebug.SetValue( m_uiState.jointDebug ); - } ); - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - ImGui::Text( "Skeleton Joint Axis" ); - ImGui::TableNextColumn(); - OnChange( ImGui::Checkbox( "##skeletonjointaxis", &m_uiState.jointAxisDebug ), [&appState, this]() { - appState.modelState.jointAxisDebug.SetValue( m_uiState.jointAxisDebug ); - } ); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - - ImGui::Text( "Skeleton Bones" ); - ImGui::TableNextColumn(); - OnChange( ImGui::Checkbox( "##skeletonbones", &m_uiState.boneDebug ), [&appState, this]() { - appState.modelState.boneDebug.SetValue( m_uiState.boneDebug ); - } ); - ImGui::TableNextRow(); - ImGui::EndTable(); - - ImGui::SeparatorText( "Animation Owners" ); - bool pressed = ImGui::FontAwesomeButton( ICON_FA_SQUARE_PLUS, 0, ImGui::GetContentRegionAvail().x ); - OnChange( pressed, [this, &appState]() { - auto* path = this->FileOpenDialog(); - if( path != nullptr ) - { - auto data = CmfContentLoader::LoadContentFromFile( path ); - if( data ) - { - appState.modelState.animationOverrides.AddState( data ); - } - } - } ); - ImGui::SetItemTooltip( "Adds an animation owner from a cmf file" ); - - for( int32_t index = 0; index < skeletonOwners.size(); ++index ) - { - ImGui::PushID( index ); - - auto skeletonOwner = skeletonOwners[index]; - - if( ImGui::RadioButton( skeletonOwner.shortSource.c_str(), appState.modelState.activeAnimationOwner.GetValue() == skeletonOwner.cmfContent ) ) - { - appState.modelState.activeAnimationOwner.SetValue( skeletonOwner.cmfContent ); - } - - if( ImGui::BeginItemTooltip() ) - { - if( index == 0 ) - { - ImGui::Text( "Model skeleton and animations" ); - } - else - { - ImGui::Text( "Animation owner from %s", skeletonOwner.source.c_str() ); - } - ImGui::Text( "Skeletons" ); - for( const auto& skeleton : skeletonOwner.skeletons ) - { - ImGui::BulletText( "%s has %d bones", skeleton.name.c_str(), skeleton.bonesCount ); - } - ImGui::EndTooltip(); - } - - if( index != 0 ) - { - ImGui::SameLine( ImGui::GetContentRegionAvail().x - BUTTON_WIDTH ); - OnChange( ImGui::FontAwesomeButton( ICON_FA_SQUARE_MINUS ), [&appState, skeletonOwner, index]() { - appState.modelState.animationOverrides.RemoveAt( index - 1 ); - - if( appState.modelState.activeAnimationOwner.GetValue() == skeletonOwner.cmfContent ) - { - appState.modelState.activeAnimationOwner.SetValue( appState.cmfContent.GetValue() ); - } - } ); - ImGui::SetItemTooltip( "Removes %s ", skeletonOwner.source.c_str() ); - } - - ImGui::PopID(); - } - } -} - -void UIRenderer::SetupSkeletons( const std::vector& skeletonStates, AppState& appState ) -{ - for( const auto& skeleton : skeletonStates ) - { - if( ImGui::TreeNode( skeleton.name.c_str() ) ) - { - if( ImGui::BeginTable( "##table", 3 ) ) - { - ImGui::TableNextRow(); - - ImGui::TableNextColumn(); - - ImGui::Text( "Bone Count" ); - ImGui::TableNextColumn(); - ImGui::Text( "%u", skeleton.bonesCount ); - } - ImGui::TreePop(); - } - } -} - -void UIRenderer::SetupMenubar( AppState& appState ) -{ - if( ImGui::BeginMainMenuBar() ) - { - if( ImGui::BeginMenu( "File" ) ) - { - if( ImGui::MenuItem( "Open", "Ctrl+O" ) ) - { - auto filePath = FileOpenDialog(); - if( filePath != nullptr ) - { - appState.ResetModelState(); - appState.cmfContent.ForceSetValue( CmfContentLoader::LoadContentFromFile( filePath ) ); - appState.cmfPath.SetValue( std::string( filePath ) ); - } - } - if( appState.cmfContent.GetValue() == nullptr ) - { - ImGui::BeginDisabled(); - } - if( ImGui::MenuItem( "Load Animation Override" ) ) - { - const auto* filePath = FileOpenDialog(); - if( filePath != nullptr ) - { - auto data = CmfContentLoader::LoadContentFromFile( filePath ); - if( data ) - { - appState.modelState.animationOverrides.AddState( data ); - } - } - } - if( appState.cmfContent.GetValue() == nullptr ) - { - ImGui::EndDisabled(); - } - - ImGui::Separator(); - if( ImGui::MenuItem( "Exit" ) ) - { - appState.exitRequested.SetValue( true ); - } - ImGui::EndMenu(); - } - auto content = appState.cmfContent.GetValue(); - - if( ImGui::BeginMenu( "View" ) ) - { - if( ImGui::BeginMenu( "Camera", content != nullptr ) ) - { - std::vector> focusObjects; - if( content != nullptr ) - { - const auto& meshes = content->m_cmfData->meshes; - - if( meshes.size() > 1 ) - { - focusObjects.push_back( { "Whole Model", content->GetBoundingSphere() } ); - } - std::for_each( meshes.begin(), meshes.end(), [&focusObjects]( const cmf::Mesh& mesh ) { - focusObjects.push_back( { cmf::ToStdString( mesh.name ), CcpMath::Sphere( mesh.bounds ) } ); - } ); - } - - uint32_t index = 0; - for( const auto& [menuName, sphere] : focusObjects ) - { - std::string menuLabel = "Focus on " + menuName; - const char* shortcut = nullptr; - if( index == 0 ) - { - shortcut = "Ctrl+F"; - } - if( ImGui::MenuItem( menuLabel.c_str(), shortcut ) ) - { - appState.cameraFocus.ForceSetValue( sphere ); - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_FOCUS ); - } - ++index; - } - ImGui::Separator(); - if( ImGui::MenuItem( "Look Right (+X)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_RIGHT ); - } - if( ImGui::MenuItem( "Look Left (-X)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_LEFT ); - } - if( ImGui::MenuItem( "Look Up (+Y)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_UP ); - } - if( ImGui::MenuItem( "Look Down (-Y)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_DOWN ); - } - if( ImGui::MenuItem( "Look Front (-Z)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_FRONT ); - } - if( ImGui::MenuItem( "Look Back (+Z)" ) ) - { - appState.cameraTrigger.ForceSetValue( CameraTrigger::CAMERA_TRIGGER_LOOK_BACK ); - } - ImGui::EndMenu(); - } - ImGui::Separator(); - const char* toggleUiLabel = m_showMainUI ? "Hide UI" : "Show UI"; - if( ImGui::MenuItem( toggleUiLabel, "Ctrl+F12" ) ) - { - ToggleUiVisibility(); - } - ImGui::EndMenu(); - } - - ImGui::EndMainMenuBar(); - } -} - -void UIRenderer::ToggleUiVisibility() -{ - m_showMainUI = !m_showMainUI; -} - -const FaIcon& UIRenderer::GetPlaybackButtonIcon() const -{ - if( m_playback.playing ) - { - return ICON_FA_PAUSE; - } - return ICON_FA_PLAY; -} - -void UIRenderer::HandlePlaybackButtonPressed() -{ - m_playback.playing = !m_playback.playing; - if( m_playback.currentTime == m_playback.duration ) - { - m_playback.currentTime = 0.0f; - } -} - -void UIRenderer::StepAnimation( float amount, AppState& appState ) -{ - m_playback.playing = false; - // step a tenth of a sec - m_playback.currentTime += amount; - m_playback.currentTime = std::max( 0.0f, std::min( m_playback.currentTime, m_playback.duration ) ); - appState.modelState.currentAnimationTime.SetValue( m_playback.currentTime ); -} - -void UIRenderer::SetupPlaybackControls( AppState& appState ) -{ - float width = (float)appState.windowSize.GetValue().first; - float height = (float)appState.windowSize.GetValue().second; - - // animation player - ImGui::SetNextWindowPos( ImVec2( 0, height - ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); - ImGui::SetNextWindowSize( ImVec2( width, ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); - bool open = true; - if( ImGui::Begin( "Animation", &open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize ) ) - { - ImGui::PushItemWidth( 120.0f ); - // selection box to the right - SetupCombo( "##animation", m_playback.animationComboBox, appState.modelState.currentAnimation ); - ImGui::SameLine(); - - bool disabled = m_playback.animationComboBox.selectedItemValue == ""; - if( disabled ) - { - ImGui::BeginDisabled(); - } - OnChange( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_LEFT ), [this, &appState]() { - StepAnimation( -0.1f, appState ); - } ); - ImGui::SameLine(); - OnChange( ImGui::FontAwesomeButton( GetPlaybackButtonIcon() ), [this]() { HandlePlaybackButtonPressed(); } ); - - ImGui::SameLine(); - OnChange( ImGui::FontAwesomeButton( ICON_FA_CHEVRON_RIGHT ), [this, &appState]() { - StepAnimation( 0.1f, appState ); - } ); - - ImGui::SameLine(); - ImGui::PushItemWidth( 30.0f ); - bool repeatPressed = m_playback.repeat ? ImGui::FontAwesomeButton( ICON_FA_REPEAT ) : ImGui::FontAwesomeSlashedButton( ICON_FA_REPEAT ); - OnChange( repeatPressed, [this]() { - m_playback.repeat = !m_playback.repeat; - } ); - ImGui::SetItemTooltip( "Repeat the animation" ); - - ImGui::SameLine(); - float availableWidth = ImGui::GetContentRegionAvail().x; - ImGui::PushItemWidth( availableWidth ); - OnChange( ImGui::SliderFloat( "##playbackSlider", &m_playback.currentTime, 0.0f, m_playback.duration ), [&appState, this]() { - appState.modelState.currentAnimationTime.SetValue( m_playback.currentTime ); - } ); - ImGui::SetItemTooltip( "%.3fs / %.3fs", m_playback.currentTime, m_playback.duration ); - - if( disabled ) - { - ImGui::EndDisabled(); - } - ImGui::SameLine(); - } - ImGui::End(); -} void UIRenderer::SetupPopupWindows( AppState& appState ) { @@ -1110,31 +195,9 @@ void UIRenderer::SetupPopupWindows( AppState& appState ) void UIRenderer::Update( AppState& appstate ) { - UpdatePlayback( appstate ); UpdateInputs( appstate ); } -void UIRenderer::UpdatePlayback( AppState& appState ) -{ - if( m_playback.playing ) - { - m_playback.currentTime += ImGui::GetIO().DeltaTime; - appState.modelState.currentAnimationTime.SetValue( m_playback.currentTime ); - if( m_playback.currentTime >= m_playback.duration ) - { - if( m_playback.repeat ) - { - m_playback.currentTime = 0.0f; - } - else - { - m_playback.currentTime = m_playback.duration; - m_playback.playing = false; - } - } - } -} - void UIRenderer::UpdateInputs( AppState& appState ) { auto io = ImGui::GetIO(); @@ -1157,12 +220,10 @@ void UIRenderer::UpdateInputs( AppState& appState ) if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_O ) ) { // file open - auto filePath = FileOpenDialog(); + auto filePath = ImGui::OpenCmfFileDialog(); if( filePath != nullptr ) { - appState.ResetModelState(); - appState.cmfContent.ForceSetValue( CmfContentLoader::LoadContentFromFile( filePath ) ); - appState.cmfPath.SetValue( std::string( filePath ) ); + appState.cmfLoadRequest.SetValue( filePath ); } } const auto content = appState.cmfContent.GetValue(); @@ -1177,193 +238,9 @@ void UIRenderer::UpdateInputs( AppState& appState ) } if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_F12 ) ) { - ToggleUiVisibility(); - } -} - -void UIRenderer::UpdateUiState( AppState& appState ) -{ - if( m_loadStatus == LoadStatus::SUCCESSFUL ) - { - // reset all appState items related to cmf - appState.modelState.selectedLod.SetValue( -1 ); - appState.modelState.currentAnimation.SetValue( "" ); - appState.modelState.currentAnimationTime.SetValue( 0.0f ); - m_playback = Playback{}; - } - - m_uiState = UiState{}; - auto cmfContent = appState.cmfContent.GetValue(); - if( cmfContent != nullptr ) - { - m_uiState.filePath = appState.cmfPath.GetValue(); - - size_t meshIndex = 0; - size_t morphIndex = 0; - size_t maxLod = 0; - uint32_t skeletonIndex = 0; - - - m_uiState.jointDebug = appState.modelState.jointDebug.GetValue(); - m_uiState.jointAxisDebug = appState.modelState.jointAxisDebug.GetValue(); - m_uiState.boneDebug = appState.modelState.boneDebug.GetValue(); - - auto addSkeletonOwner = [this]( const std::string& source, const std::string& shortNameOverride, std::shared_ptr data ) { - SkeletonOwnerUiState skeletonOwnerState{}; - skeletonOwnerState.source = source; - skeletonOwnerState.cmfContent = data; - if( shortNameOverride.empty() ) - { - skeletonOwnerState.shortSource = source; - skeletonOwnerState.shortSource = skeletonOwnerState.shortSource.substr( skeletonOwnerState.shortSource.find_last_of( "/\\" ) + 1 ); - skeletonOwnerState.shortSource = skeletonOwnerState.shortSource.substr( skeletonOwnerState.shortSource.find_last_of( "//" ) + 1 ); - } - else - { - skeletonOwnerState.shortSource = shortNameOverride; - } - for( const auto& skeleton : data->m_cmfData->skeletons ) - { - SkeletonUiState skeletonState{}; - skeletonState.name = cmf::ToStdString( skeleton.name ); - skeletonState.bonesCount = static_cast( skeleton.bones.size() ); - skeletonOwnerState.skeletons.push_back( skeletonState ); - } - m_uiState.skeletonOwners.push_back( skeletonOwnerState ); - }; - - // Add skeleton owners - addSkeletonOwner( cmfContent->m_filePath, "Model Skeleton", cmfContent ); - for( const auto& animationOverride : appState.modelState.animationOverrides ) - { - addSkeletonOwner( animationOverride.GetValue()->m_filePath, "", animationOverride.GetValue() ); - } - - for( const auto& mesh : cmfContent->m_cmfData->meshes ) - { - if( meshIndex >= appState.modelState.meshes.size() ) - { - break; - } - const auto& meshAppState = appState.modelState.meshes[meshIndex].GetValue(); - MeshUiState meshState{}; - meshState.meshIndex = meshIndex; - meshState.name = cmf::ToStdString( mesh.name ); - meshState.lod = meshAppState.activeLod.GetValue(); - meshState.maxLodIndex = static_cast( mesh.lods.size() ) - 1; - meshState.boundingSphereRadius = CcpMath::Sphere( mesh.bounds ).radius; - meshState.screenSize = meshAppState.meshScreenSize.GetValue(); - meshState.display = meshAppState.display.GetValue(); - meshState.wireframeOverlay = meshAppState.wireframeOverlay.GetValue(); - meshState.audioOcclusionMesh = meshAppState.audioOcclusionMesh.GetValue(); - meshState.hasAudioOcclusionMesh = !mesh.audioOcclusionMesh.vertices.empty() && !mesh.audioOcclusionMesh.indices.empty(); - meshState.boundingBox = meshAppState.renderBoundingBox.GetValue(); - - maxLod = std::max( maxLod, mesh.lods.size() ); - meshState.vertexCount = 0; - meshState.indexCount = 0; - if( meshState.lod < mesh.lods.size() ) - { - const auto& lod = mesh.lods[meshState.lod]; - if( lod.vb.stride > 0 ) - { - meshState.vertexCount = cmf::GetStreamElementCount( lod.vb ); - } - if( lod.ib.stride > 0 ) - { - meshState.indexCount = cmf::GetStreamElementCount( lod.ib ); - } - } - - m_uiState.modelStates.vertexCount += meshState.vertexCount; - m_uiState.modelStates.indexCount += meshState.indexCount; - - for( size_t i = 0; i < mesh.morphTargets.targets.size(); ++i ) - { - if( i >= meshAppState.morphs.size() ) - { - break; - } - const auto& morphTarget = mesh.morphTargets.targets[i]; - const auto& morphPair = meshAppState.morphs[i].GetValue(); - MorphTargetUiState morphTargetState{}; - morphTargetState.name = cmf::ToStdString( morphTarget.name ); - morphTargetState.weight = morphPair.first; - morphTargetState.enabled = morphPair.second; - morphTargetState.index = i; - meshState.morphTargets.push_back( morphTargetState ); - } - - m_uiState.modelStates.meshes.push_back( meshState ); - meshIndex++; - } - - // visualization shader selection - for( const auto& shaderSetupInfo : appState.modelState.availableShaders.GetValue() ) - { - std::string shaderName = shaderSetupInfo.first; - if( !shaderSetupInfo.second.shaderNameAddition.empty() ) - { - shaderName += " " + shaderSetupInfo.second.shaderNameAddition; - } - - m_uiState.visualizationShaderComboBox.items.push_back( { shaderName, shaderSetupInfo } ); - } - m_uiState.visualizationShaderComboBox.SetSelectedItemByValue( appState.modelState.activeShader.GetValue() ); - - m_uiState.modelStates.lod.items.push_back( std::make_pair( "Auto", -1 ) ); - - for( uint32_t lod = 0; lod < maxLod; ++lod ) - { - m_uiState.modelStates.lod.items.push_back( std::make_pair( "Lod " + std::to_string( lod ), lod ) ); - } - - m_uiState.modelStates.lod.SetSelectedItemByValue( appState.modelState.selectedLod.GetValue() ); - m_uiState.modelStates.boundingBox = appState.modelState.modelBoundingBox.GetValue(); - - if( m_playback.animationComboBox.items.empty() ) - { - m_playback.animationComboBox.items.push_back( std::make_pair( "Rest Pose", "" ) ); - auto animationOwner = appState.modelState.activeAnimationOwner.GetValue(); - if( animationOwner ) - { - cmf::Data* activeData = animationOwner->m_cmfData; - - for( const auto& animation : activeData->animations ) - { - auto animationName = cmf::ToStdString( animation.name ); - m_playback.animationComboBox.items.push_back( std::make_pair( animationName, animationName ) ); - if( appState.modelState.currentAnimation.GetValue() == animationName ) - { - m_playback.duration = animation.duration; - } - } - m_playback.animationComboBox.SetSelectedItemByValue( appState.modelState.currentAnimation.GetValue() ); - } - } - } - else - { - // default uninitialized values - if( m_playback.animationComboBox.items.empty() ) - { - m_playback.animationComboBox.items.push_back( std::make_pair( "Rest Pose", "" ) ); - m_playback.animationComboBox.SetSelectedItemByValue( appState.modelState.currentAnimation.GetValue() ); - } - - m_uiState.filePath = "No file loaded"; - - // visualization shader selection - m_uiState.visualizationShaderComboBox.items.push_back( { "", {} } ); + m_menuState.showUi = !m_menuState.showUi; } } -void UIRenderer::OnChange( bool changed, std::function callback ) -{ - if( changed ) - { - callback(); - } -} // NOLINTEND(cppcoreguidelines-pro-type-vararg) \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiRenderer.h b/src/viewer/rendering/ui/uiRenderer.h index c2b06b3..928d39d 100644 --- a/src/viewer/rendering/ui/uiRenderer.h +++ b/src/viewer/rendering/ui/uiRenderer.h @@ -10,18 +10,11 @@ #include "appState.h" #include "rendering/renderer.h" #include "uiDetailWindow.h" +#include "uiMenubar.h" +#include "uiGeneralWindow.h" +#include "uiAnimationPlayback.h" #include "../vulkan/commandbuffer.h" - -namespace ImGui -{ -enum class CheckBoxTriStateValue -{ - UNCHECKED = 0, - CHECKED = 1, - MIXED = -1 -}; -bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ); -} +#include "uiCustomWidgets.h" // Handles rendering the UI class UIRenderer @@ -37,22 +30,11 @@ class UIRenderer void SetupUi( AppState& appState ); void CMFInfoWindow( AppState& appState ); - void SetupMenubar( AppState& appState ); void Update( AppState& state ); private: + void SetupPopupWindows( AppState& appState ); void UpdateInputs( AppState& state ); - void UpdatePlayback( AppState& appState ); - - template - struct CmfUiComboBox - { - std::string selectedItemName; - T selectedItemValue; - std::vector> items; - - void SetSelectedItemByValue( T value ); - }; enum LoadStatus { @@ -61,120 +43,19 @@ class UIRenderer NOTHING_LOADED }; - struct MorphTargetUiState - { - std::string name{ "" }; - float weight{ 0.0f }; - bool enabled{ true }; - size_t index; - }; - - struct MeshUiState - { - std::string name; - size_t meshIndex{ 0 }; - uint32_t lod{ 0 }; - uint32_t maxLodIndex{ 0 }; - float screenSize{ 0 }; - float boundingSphereRadius{ 0 }; - uint32_t vertexCount{ 0 }; - uint32_t indexCount{ 0 }; - std::vector morphTargets; - bool display{ true }; - bool boundingBox{ false }; - bool wireframeOverlay{ false }; - bool audioOcclusionMesh{ false }; - bool hasAudioOcclusionMesh{ false }; - std::vector> showVertexNormals{}; - std::vector> showVertexTangents{}; - std::vector> showVertexBinormals{}; - }; - - struct ModelUiState - { - std::vector meshes; - CmfUiComboBox lod; - uint32_t vertexCount{ 0 }; - uint32_t indexCount{ 0 }; - bool boundingBox{ false }; - }; - - struct SkeletonUiState - { - uint32_t bonesCount{ 0 }; - std::string name; - }; - - struct SkeletonOwnerUiState - { - std::string source; - std::string shortSource; - std::shared_ptr cmfContent; - std::vector skeletons; - }; - - struct UiState - { - std::string filePath; - ModelUiState modelStates{}; - std::vector skeletonOwners{}; - CmfUiComboBox> visualizationShaderComboBox; - bool boneDebug{ false }; - bool jointDebug{ false }; - bool jointAxisDebug{ false }; - }; - - struct Playback - { - float duration{ 0.0f }; - float currentTime{ 0.0f }; - bool playing{ false }; - bool repeat{ false }; - CmfUiComboBox animationComboBox; - }; - - void RegisterModelCallbacks( AppState& appState ); - - void SetupGeneralView( AppState& appState ); - void SetupMeshListView( const ModelUiState& modelState, AppState& appState ); - void SetupMeshView( const MeshUiState& mesh, AppState& appState ); - void SetupModelAxisRows( AppState& appState ); - - template - void SetupModelAxisRow( std::vector>& checkboxStates, const std::string& name, const Callable& changeCallback ); - - std::vector> GetAxisTriCheckboxStates( const std::vector>>& axisStates ); - void SetupVertexAxisRows( MeshState& meshAppState ); - void SetupVertexAxisRow( StateCollection>& vertexAxisStates, const char* label, int columnCount ); - void SetupMorphTarget( const MorphTargetUiState& morphTarget, size_t meshIndex, AppState& appState ); - void SetupSkeletonOwners( const std::vector& skeletonOwners, AppState& appState ); - void SetupSkeletons( const std::vector& skeletonStates, AppState& appState ); - void SetupPlaybackControls( AppState& appState ); - void SetupPopupWindows( AppState& appState ); - - const FaIcon& GetPlaybackButtonIcon() const; - void HandlePlaybackButtonPressed(); - void StepAnimation( float amount, AppState& appState ); - - void OnChange( bool changed, std::function callback ); - - template - void SetupCombo( const char* name, UIRenderer::CmfUiComboBox& combo, State& applicableState ); - void UpdateUiState( AppState& appState ); - void ToggleUiVisibility(); - const char* FileOpenDialog(); VkDescriptorPool m_descriptorPool{ VK_NULL_HANDLE }; std::shared_ptr m_renderer; GraphicsCommandBuffer m_graphicsCommandBuffer; - UiState m_uiState{}; - bool m_showMainUI{ true }; LoadStatus m_loadStatus{ NOTHING_LOADED }; - Playback m_playback{}; + UIGeneralWindow m_generalWindow{}; UIDetailWindow m_detailWindow{}; + UIAnimationPlayback m_animationPlayback{}; + UiMenubar m_menubar{}; + MenuState m_menuState{}; }; diff --git a/src/viewer/rendering/ui/uiRenderer_template_impl.h b/src/viewer/rendering/ui/uiRenderer_template_impl.h index b272bce..e69de29 100644 --- a/src/viewer/rendering/ui/uiRenderer_template_impl.h +++ b/src/viewer/rendering/ui/uiRenderer_template_impl.h @@ -1,99 +0,0 @@ -// Copyright © 2026 CCP ehf. - -template -void UIRenderer::SetupCombo( const char* name, UIRenderer::CmfUiComboBox& combo, State& applicableState ) -{ - if( ImGui::BeginCombo( name, combo.selectedItemName.c_str() ) ) - { - for( const auto& nameValue : combo.items ) - { - bool is_selected = ( nameValue.second == combo.selectedItemValue ); - - if( ImGui::Selectable( nameValue.first.c_str(), is_selected ) ) - { - combo.SetSelectedItemByValue( nameValue.second ); - applicableState.SetValue( nameValue.second ); - } - - if( is_selected ) - { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } -} - -template -void UIRenderer::CmfUiComboBox::SetSelectedItemByValue( T value ) -{ - for( const auto& item : items ) - { - if( item.second == value ) - { - selectedItemName = item.first; - selectedItemValue = item.second; - return; - } - } - if( !items.empty() ) - { - selectedItemName = items.front().first; - selectedItemValue = items.front().second; - } -} - -template -void UIRenderer::SetupModelAxisRow( std::vector>& checkboxStates, const std::string& name, const Callable& changeCallback ) -{ - int32_t maxIndex = checkboxStates.empty() ? -1 : std::max_element( checkboxStates.begin(), checkboxStates.end(), []( const std::pair& a, const std::pair& b ) { - return a.first < b.first; - } )->first; - - ImGui::BeginDisabled( maxIndex < 0 ); - ImGui::TableNextColumn(); - ImGui::Text( "%s", name.c_str() ); - ImGui::SetItemTooltip( "Toggles the %s visualization for all meshes", name.c_str() ); - ImGui::TableNextColumn(); - - if( maxIndex >= 0 ) - { - for( int32_t i = 0; i <= maxIndex; ++i ) - { - auto foundState = std::find_if( checkboxStates.begin(), checkboxStates.end(), [i]( const std::pair& pair ) { - return pair.first == (uint32_t)i; - } ); - - if( foundState == checkboxStates.end() ) - { - // add a disabled checkbox - ImGui::BeginDisabled( true ); - auto disabledValue = ImGui::CheckBoxTriStateValue::UNCHECKED; - ImGui::CheckBoxTristate( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &disabledValue ); - - ImGui::SetItemTooltip( "No mesh has %s information for usage %d", name.c_str(), i ); - ImGui::SameLine(); - ImGui::EndDisabled(); - } - else - { - auto value = foundState->second; - OnChange( ImGui::CheckBoxTristate( ( std::string( "##tricheckbox" ) + name + std::to_string( i ) ).c_str(), &value ), [changeCallback, i, value]() { - changeCallback( value != ImGui::CheckBoxTriStateValue::CHECKED, i ); - } ); - ImGui::SetItemTooltip( "Toggles debug visualization for %s with usage index %d for all meshes (if available)", name.c_str(), i ); - ImGui::SameLine(); - } - } - } - if( maxIndex < 0 ) - { - ImGui::BeginDisabled( true ); - bool value = false; - ImGui::Checkbox( ( std::string( "##" ) + name ).c_str(), &value ); - ImGui::SetItemTooltip( "Model doesn't have any %s", name ); - ImGui::SameLine(); - ImGui::EndDisabled(); - } - ImGui::EndDisabled(); -} \ No newline at end of file From 03ef3f36db6b3ed297ae22fea067ba49a9c5e729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 12:40:09 +0000 Subject: [PATCH 12/18] Refactor UI and font handling; update icon size types - Fixed formatting in Camera::GetProjection(). - Adjusted ImGui item width scoping in UIAnimationPlayback::Render(). - Improved font memory management in UIRenderer::Initialize(). - Changed icon size handling in fontAwesomeCreator.py to use floats. --- src/viewer/rendering/camera.cpp | 2 +- src/viewer/rendering/ui/uiAnimationPlayback.cpp | 4 ++-- src/viewer/rendering/ui/uiRenderer.cpp | 3 ++- src/viewer/scripts/fontAwesomeCreator.py | 16 ++++++++-------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/viewer/rendering/camera.cpp b/src/viewer/rendering/camera.cpp index 7ca7d8a..160e37a 100644 --- a/src/viewer/rendering/camera.cpp +++ b/src/viewer/rendering/camera.cpp @@ -116,7 +116,7 @@ Matrix Camera::GetProjection() const const float calculatedFar = centerDepth + boundingDepth; const float farPlane = std::max( ABSOLUTE_MIN_FAR_PLANE, calculatedFar ); - const float nearPlane = std::max( farPlane / 100.0f , centerDepth - boundingDepth ); + const float nearPlane = std::max( farPlane / 100.0f, centerDepth - boundingDepth ); return PerspectiveFovMatrix( m_fov, diff --git a/src/viewer/rendering/ui/uiAnimationPlayback.cpp b/src/viewer/rendering/ui/uiAnimationPlayback.cpp index 2555eb5..87c3aef 100644 --- a/src/viewer/rendering/ui/uiAnimationPlayback.cpp +++ b/src/viewer/rendering/ui/uiAnimationPlayback.cpp @@ -18,8 +18,6 @@ void UIAnimationPlayback::Render( AppState& appState ) if( ImGui::Begin( "##animationTitle", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize ) ) { // animation selection - ImGui::PushItemWidth( ANIMATION_SELECTION_WIDTH ); - std::vector animationNames; std::vector animationDuration; const auto& activeAnimationOwner = appState.modelState.activeAnimationOwner.GetValue(); @@ -64,6 +62,7 @@ void UIAnimationPlayback::Render( AppState& appState ) ImGui::BeginDisabled( animationNames.empty() ); + ImGui::PushItemWidth( ANIMATION_SELECTION_WIDTH ); ImGui::ComboBox( "##animation", animationNames, selectedAnimationIndex, [&]( int32_t selectedIndex ) { m_playing = false; m_currentTime = 0.0f; @@ -79,6 +78,7 @@ void UIAnimationPlayback::Render( AppState& appState ) } appState.modelState.currentAnimationTime.SetValue( m_currentTime ); } ); + ImGui::PopItemWidth(); ImGui::SetItemTooltip( "Select an animation to play" ); ImGui::SameLine(); diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index 25a7362..3ee5b36 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -53,8 +53,9 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) ImFontConfig config; config.MergeMode = true; + config.FontDataOwnedByAtlas = false; // add fontawesome icons - io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, fa_solid_900_ttf_size, UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast(fa_solid_900_ttf_size), UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); // Setup Dear ImGui style ImGui::StyleColorsDark(); diff --git a/src/viewer/scripts/fontAwesomeCreator.py b/src/viewer/scripts/fontAwesomeCreator.py index 22b7dc2..1ddd20e 100644 --- a/src/viewer/scripts/fontAwesomeCreator.py +++ b/src/viewer/scripts/fontAwesomeCreator.py @@ -80,19 +80,19 @@ def _format_ratio(width: int, height: int) -> str: return f"{formatted}f" -def _read_icon_size(payload: Dict[str, object]) -> Tuple[int, int]: +def _read_icon_size(payload: Dict[str, object]) -> Tuple[float, float]: svg_payload = payload.get("svg") if not isinstance(svg_payload, dict): - return 0, 0 + return 0.0, 0.0 solid_payload = svg_payload.get("solid") if not isinstance(solid_payload, dict): - return 0, 0 + return 0.0, 0.0 width = solid_payload.get("width") height = solid_payload.get("height") - if isinstance(width, int) and isinstance(height, int): - return width, height + if isinstance(width, (int, float)) and isinstance(height, (int, float)): + return float(width), float(height) view_box = solid_payload.get("viewBox") if isinstance(view_box, list) and len(view_box) >= 4: @@ -101,14 +101,14 @@ def _read_icon_size(payload: Dict[str, object]) -> Tuple[int, int]: return float(view_box_width), float(view_box_height) - return 0, 0 + return 0.0, 0.0 -def _load_icons(icons_json_path: Path) -> List[Tuple[str, int, int, int]]: +def _load_icons(icons_json_path: Path) -> List[Tuple[str, int, float, float]]: with icons_json_path.open("r", encoding="utf-8") as input_file: icon_data: Dict[str, Dict[str, object]] = json.load(input_file) - icons: List[Tuple[str, int, int, int]] = [] + icons: List[Tuple[str, int, float, float]] = [] for icon_name, payload in icon_data.items(): styles = payload.get("styles", []) unicode_hex = payload.get("unicode") From 2459eefa6fb31e7822453975bf0fa1d886a09c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 12:41:58 +0000 Subject: [PATCH 13/18] Fixing a mac build error --- src/viewer/rendering/ui/uiGeneralWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp index 7fb3540..41bad19 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.cpp +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -125,7 +125,7 @@ void SetupModelAxisRow( AxisTriCheckboxStates& checkboxStates, const char* name, ImGui::BeginDisabled( true ); bool value = false; ImGui::Checkbox( ( std::string( "##" ) + name ).c_str(), &value ); - ImGui::SetItemTooltip( "Model doesn't have any %s", name ); + ImGui::SetItemTooltip( "Model doesn't have any %s", name.c_str() ); ImGui::SameLine(); ImGui::EndDisabled(); } From bbdc58312a9eb71eb994eb128bcd40ee241b2d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 12:44:23 +0000 Subject: [PATCH 14/18] Fixing linter errors --- src/viewer/appState.cpp | 1 - src/viewer/application.cpp | 2 +- src/viewer/main.cpp | 2 +- src/viewer/rendering/ui/uiRenderer.cpp | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/viewer/appState.cpp b/src/viewer/appState.cpp index ab502bc..f6ad7c2 100644 --- a/src/viewer/appState.cpp +++ b/src/viewer/appState.cpp @@ -31,4 +31,3 @@ void AppState::ResetModelState() { modelState = {}; } - diff --git a/src/viewer/application.cpp b/src/viewer/application.cpp index 460acec..8db37e9 100644 --- a/src/viewer/application.cpp +++ b/src/viewer/application.cpp @@ -206,7 +206,7 @@ void Application::Run() m_sceneRenderer->Render( m_appState, m_camera ); m_orientationGizmoRenderer->Render( m_appState, m_camera ); - + m_uiRenderer->Update( m_appState ); m_uiRenderer->Render( m_appState ); diff --git a/src/viewer/main.cpp b/src/viewer/main.cpp index 6c66d95..c48c467 100644 --- a/src/viewer/main.cpp +++ b/src/viewer/main.cpp @@ -22,7 +22,7 @@ int main( int argc, char** argv ) app.Initialize(); if( argc > 1 ) - { + { for( int i = 1; i < argc; ++i ) { std::cout << "Loading file: " << argv[i] << std::endl; diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index 3ee5b36..a478586 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -55,7 +55,7 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) config.MergeMode = true; config.FontDataOwnedByAtlas = false; // add fontawesome icons - io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast(fa_solid_900_ttf_size), UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast( fa_solid_900_ttf_size ), UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); // Setup Dear ImGui style ImGui::StyleColorsDark(); From 8605f737c4005d501cb46d88d8a7e9aae8f7c440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 13:06:09 +0000 Subject: [PATCH 15/18] Fixing more clang errors --- src/viewer/rendering/ui/uiCustomWidgets.cpp | 2 +- src/viewer/rendering/ui/uiGeneralWindow.cpp | 8 ++++---- src/viewer/rendering/ui/uiGeneralWindow.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/viewer/rendering/ui/uiCustomWidgets.cpp b/src/viewer/rendering/ui/uiCustomWidgets.cpp index b870a40..72a5489 100644 --- a/src/viewer/rendering/ui/uiCustomWidgets.cpp +++ b/src/viewer/rendering/ui/uiCustomWidgets.cpp @@ -77,7 +77,7 @@ void FontAwesomeText( const FaIcon& icon, float width ) const float glyphWidth = UiConsts::FONT_AWESOME_SIZE * icon.xyRatio; const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); - ImGui::Text( icon.text ); + ImGui::TextUnformatted( icon.text ); ImGui::PopStyleVar(); } diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp index 41bad19..5b3a7a9 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.cpp +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -54,7 +54,7 @@ AxisTriCheckboxStates GetAxisTriCheckboxStates( StateCollection& mesh { auto [index, checked] = state; - auto foundState = std::find_if( combinedStates.begin(), combinedStates.end(), [&]( const auto& existingState ) { + auto foundState = std::find_if( combinedStates.begin(), combinedStates.end(), [&index]( const auto& existingState ) { return existingState.first == index; } ); auto stateAsTriState = checked ? ImGui::CheckBoxTriStateValue::CHECKED : ImGui::CheckBoxTriStateValue::UNCHECKED; @@ -125,7 +125,7 @@ void SetupModelAxisRow( AxisTriCheckboxStates& checkboxStates, const char* name, ImGui::BeginDisabled( true ); bool value = false; ImGui::Checkbox( ( std::string( "##" ) + name ).c_str(), &value ); - ImGui::SetItemTooltip( "Model doesn't have any %s", name.c_str() ); + ImGui::SetItemTooltip( "Model doesn't have any %s", name ); ImGui::SameLine(); ImGui::EndDisabled(); } @@ -277,7 +277,7 @@ void UIGeneralWindow::RenderGeneralInfo( AppState& appState ) } ); SetupAttribute( "Vertices", "Vertex count of all meshes", false, [&]() { ImGui::Text( "%u", vertexCount ); } ); SetupAttribute( "Indices", "Index count of all meshes", false, [&]() { ImGui::Text( "%u", indexCount ); } ); - SetupAttribute( "Meshes", "Number of meshes", false, [&]() { ImGui::Text( "%u", data ? data->m_cmfData->meshes.size() : 0 ); } ); + SetupAttribute( "Meshes", "Number of meshes", false, [&]() { ImGui::Text( "%zu", data ? data->m_cmfData->meshes.size() : 0 ); } ); SetupAttribute( "Visualization", "Visualization mode", data == nullptr, [&]() { const auto& availableShaders = appState.modelState.availableShaders.GetValue(); @@ -669,7 +669,7 @@ void UIGeneralWindow::RenderAnimationOverrideList( AppState& appState ) ImGui::Text( "Skeletons" ); for( const auto& skeleton : animationOwner->m_cmfData->skeletons ) { - ImGui::BulletText( "%s has %d bones", cmf::ToStdString( skeleton.name ), skeleton.bones.size() ); + ImGui::BulletText( "%s has %zu bones", cmf::ToStdString( skeleton.name ).c_str(), skeleton.bones.size() ); } ImGui::EndTooltip(); } diff --git a/src/viewer/rendering/ui/uiGeneralWindow.h b/src/viewer/rendering/ui/uiGeneralWindow.h index 1eb588e..3599aca 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.h +++ b/src/viewer/rendering/ui/uiGeneralWindow.h @@ -39,7 +39,7 @@ class UIGeneralWindow constructor(); if( tooltip ) { - ImGui::SetItemTooltip( tooltip ); + ImGui::SetItemTooltip( "%s", tooltip ); } ImGui::EndDisabled(); } From 18c936f95a4752717d511f7efcf781d68ff73c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 13:21:48 +0000 Subject: [PATCH 16/18] hopefully fixing a mac compile error --- src/viewer/rendering/ui/uiGeneralWindow.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp index 5b3a7a9..f2d12c9 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.cpp +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -52,7 +52,8 @@ AxisTriCheckboxStates GetAxisTriCheckboxStates( StateCollection& mesh // combine the states for( const auto& state : axisStatesForMesh ) { - auto [index, checked] = state; + auto index = state.first; + auto checked = state.second; auto foundState = std::find_if( combinedStates.begin(), combinedStates.end(), [&index]( const auto& existingState ) { return existingState.first == index; From 5e35305de13b285d33c633a22e99884594274493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 13:38:12 +0000 Subject: [PATCH 17/18] removing an unused file --- src/viewer/rendering/ui/uiRenderer.h | 3 --- src/viewer/rendering/ui/uiRenderer_template_impl.h | 0 2 files changed, 3 deletions(-) delete mode 100644 src/viewer/rendering/ui/uiRenderer_template_impl.h diff --git a/src/viewer/rendering/ui/uiRenderer.h b/src/viewer/rendering/ui/uiRenderer.h index 928d39d..b374e63 100644 --- a/src/viewer/rendering/ui/uiRenderer.h +++ b/src/viewer/rendering/ui/uiRenderer.h @@ -57,6 +57,3 @@ class UIRenderer UiMenubar m_menubar{}; MenuState m_menuState{}; }; - - -#include "uiRenderer_template_impl.h" \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiRenderer_template_impl.h b/src/viewer/rendering/ui/uiRenderer_template_impl.h deleted file mode 100644 index e69de29..0000000 From 0fb644a00d530947f9a75b9f71e38b98d531d267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20=C3=9E=C3=B3r=20Gunnarsson?= Date: Fri, 3 Jul 2026 15:12:06 +0000 Subject: [PATCH 18/18] Fixing some code review comments --- src/viewer/appState.cpp | 1 + src/viewer/appState.h | 1 + src/viewer/main.cpp | 2 +- .../rendering/ui/uiAnimationPlayback.cpp | 2 +- src/viewer/rendering/ui/uiConsts.h | 2 + src/viewer/rendering/ui/uiGeneralWindow.cpp | 19 ++++ src/viewer/rendering/ui/uiGeneralWindow.h | 27 +----- src/viewer/rendering/ui/uiMenubar.cpp | 8 +- src/viewer/rendering/ui/uiMenubar.h | 7 +- src/viewer/rendering/ui/uiRenderer.cpp | 11 ++- src/viewer/rendering/ui/uiRenderer.h | 9 -- src/viewer/scripts/fontAwesomeCreator.py | 93 ++++++++++++++----- src/viewer/scripts/shaderCacheCreator.py | 2 + 13 files changed, 113 insertions(+), 71 deletions(-) diff --git a/src/viewer/appState.cpp b/src/viewer/appState.cpp index f6ad7c2..aed1d98 100644 --- a/src/viewer/appState.cpp +++ b/src/viewer/appState.cpp @@ -23,6 +23,7 @@ void AppState::CallStateCallbacks() cmfPath.CallCallbacks( *this ); cmfLoadRequest.CallCallbacks( *this ); exitRequested.CallCallbacks( *this ); + showUi.CallCallbacks( *this ); modelState.CallCallbacks( *this ); } diff --git a/src/viewer/appState.h b/src/viewer/appState.h index e9cccde..bb748a2 100644 --- a/src/viewer/appState.h +++ b/src/viewer/appState.h @@ -196,6 +196,7 @@ struct AppState State cmfLoadRequest{ "" }; State exitRequested{ false }; + State showUi{ true }; // model ModelState modelState{}; diff --git a/src/viewer/main.cpp b/src/viewer/main.cpp index c48c467..582466f 100644 --- a/src/viewer/main.cpp +++ b/src/viewer/main.cpp @@ -25,7 +25,7 @@ int main( int argc, char** argv ) { for( int i = 1; i < argc; ++i ) { - std::cout << "Loading file: " << argv[i] << std::endl; + Log::Info( "Loading file: %s", argv[i] ); app.LoadCmfFile( argv[i] ); } } diff --git a/src/viewer/rendering/ui/uiAnimationPlayback.cpp b/src/viewer/rendering/ui/uiAnimationPlayback.cpp index 87c3aef..04d4ff2 100644 --- a/src/viewer/rendering/ui/uiAnimationPlayback.cpp +++ b/src/viewer/rendering/ui/uiAnimationPlayback.cpp @@ -11,7 +11,7 @@ void UIAnimationPlayback::Render( AppState& appState ) { UpdateState( appState ); - const auto& [width, height] = appState.windowSize.GetValue(); + const auto [width, height] = appState.windowSize.GetValue(); ImGui::SetNextWindowPos( ImVec2( 0, static_cast( height ) - UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); ImGui::SetNextWindowSize( ImVec2( static_cast( width ), UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); diff --git a/src/viewer/rendering/ui/uiConsts.h b/src/viewer/rendering/ui/uiConsts.h index a73448c..c6395b1 100644 --- a/src/viewer/rendering/ui/uiConsts.h +++ b/src/viewer/rendering/ui/uiConsts.h @@ -1,3 +1,5 @@ +// Copyright © 2026 CCP ehf. + #pragma once #include diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp index f2d12c9..49cd8b7 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.cpp +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -16,6 +16,25 @@ namespace { +template +void SetupAttribute( const char* name, const char* tooltip, bool disabled, Callable constructor ) +{ + ImGui::TableNextRow(); + + ImGui::BeginDisabled( disabled ); + ImGui::TableNextColumn(); + + ImGui::TextUnformatted( name ); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); + constructor(); + if( tooltip ) + { + ImGui::SetItemTooltip( "%s", tooltip ); + } + ImGui::EndDisabled(); +} + using AxisTriCheckboxStates = std::vector>; using AxisCheckboxStates = std::vector>; diff --git a/src/viewer/rendering/ui/uiGeneralWindow.h b/src/viewer/rendering/ui/uiGeneralWindow.h index 3599aca..20af2a1 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.h +++ b/src/viewer/rendering/ui/uiGeneralWindow.h @@ -3,13 +3,8 @@ #pragma once #include "../../appState.h" -#include #include -#include -#include -// ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file -// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) class UIGeneralWindow { public: @@ -24,24 +19,4 @@ class UIGeneralWindow void RenderAnimationOverrideList( AppState& appState ); ImGuiTextFilter m_morphFilter; - - template - void SetupAttribute( const char* name, const char* tooltip, bool disabled, Callable constructor ) - { - ImGui::TableNextRow(); - - ImGui::BeginDisabled( disabled ); - ImGui::TableNextColumn(); - - ImGui::TextUnformatted( name ); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth( ImGui::GetContentRegionAvail().x ); - constructor(); - if( tooltip ) - { - ImGui::SetItemTooltip( "%s", tooltip ); - } - ImGui::EndDisabled(); - } -}; -// NOLINTEND(cppcoreguidelines-pro-type-vararg) \ No newline at end of file +}; \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiMenubar.cpp b/src/viewer/rendering/ui/uiMenubar.cpp index d574d07..415aacf 100644 --- a/src/viewer/rendering/ui/uiMenubar.cpp +++ b/src/viewer/rendering/ui/uiMenubar.cpp @@ -5,7 +5,7 @@ #include "uiCustomWidgets.h" -void UiMenubar::Render( AppState& appState, MenuState& menuState ) +void UiMenubar::Render( AppState& appState ) { if( ImGui::BeginMainMenuBar() ) { @@ -111,10 +111,12 @@ void UiMenubar::Render( AppState& appState, MenuState& menuState ) ImGui::EndMenu(); } ImGui::Separator(); - const char* toggleUiLabel = menuState.showUi ? "Hide UI" : "Show UI"; + + bool uiShown = appState.showUi.GetValue(); + const char* toggleUiLabel = uiShown ? "Hide UI" : "Show UI"; if( ImGui::MenuItem( toggleUiLabel, "Ctrl+F12" ) ) { - menuState.showUi = !menuState.showUi; + appState.showUi.SetValue( !uiShown ); } ImGui::EndMenu(); } diff --git a/src/viewer/rendering/ui/uiMenubar.h b/src/viewer/rendering/ui/uiMenubar.h index 31f138b..458c782 100644 --- a/src/viewer/rendering/ui/uiMenubar.h +++ b/src/viewer/rendering/ui/uiMenubar.h @@ -4,13 +4,8 @@ #include "../../appState.h" -struct MenuState -{ - bool showUi{ true }; -}; - class UiMenubar { public: - void Render( AppState& appState, MenuState& menuState ); + void Render( AppState& appState ); }; \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index a478586..ce9c47a 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -3,11 +3,14 @@ #include "uiRenderer.h" #include +#include +#include #include #include #include "appState.h" +#include "uiConsts.h" +#include "uiCustomWidgets.h" #include "../vulkan/vulkanerrors.h" -#include // ImGui is using a lot of variadic functions for text formatting, so we disable the cppcoreguidelines-pro-type-vararg lint for this file @@ -150,8 +153,8 @@ void UIRenderer::Render( AppState& appState ) void UIRenderer::SetupUi( AppState& appState ) { - m_menubar.Render( appState, m_menuState ); - if( m_menuState.showUi ) + m_menubar.Render( appState ); + if( appState.showUi.GetValue() ) { m_generalWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); m_detailWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); @@ -239,7 +242,7 @@ void UIRenderer::UpdateInputs( AppState& appState ) } if( ImGui::IsKeyChordPressed( ImGuiMod_Ctrl | ImGuiKey_F12 ) ) { - m_menuState.showUi = !m_menuState.showUi; + appState.showUi.SetValue( !appState.showUi.GetValue() ); } } diff --git a/src/viewer/rendering/ui/uiRenderer.h b/src/viewer/rendering/ui/uiRenderer.h index b374e63..6aee38b 100644 --- a/src/viewer/rendering/ui/uiRenderer.h +++ b/src/viewer/rendering/ui/uiRenderer.h @@ -1,12 +1,5 @@ // Copyright © 2026 CCP ehf. -#include - -// used by the template file -#include -#include -#include - #include "appState.h" #include "rendering/renderer.h" #include "uiDetailWindow.h" @@ -14,7 +7,6 @@ #include "uiGeneralWindow.h" #include "uiAnimationPlayback.h" #include "../vulkan/commandbuffer.h" -#include "uiCustomWidgets.h" // Handles rendering the UI class UIRenderer @@ -55,5 +47,4 @@ class UIRenderer UIDetailWindow m_detailWindow{}; UIAnimationPlayback m_animationPlayback{}; UiMenubar m_menubar{}; - MenuState m_menuState{}; }; diff --git a/src/viewer/scripts/fontAwesomeCreator.py b/src/viewer/scripts/fontAwesomeCreator.py index 1ddd20e..bcab548 100644 --- a/src/viewer/scripts/fontAwesomeCreator.py +++ b/src/viewer/scripts/fontAwesomeCreator.py @@ -1,6 +1,4 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © 2025 CCP ehf. +# Copyright © 2026 CCP ehf. """Generate ImGui-readable Font Awesome headers. @@ -13,8 +11,6 @@ by emitting a `unsigned int` data array and explicit byte size. """ -from __future__ import annotations - import json import re import sys @@ -22,13 +18,19 @@ from typing import Dict, Iterable, List, Tuple -def _find_repo_root() -> Path: +def _find_repo_root(): """Resolve repository root from this script location.""" return Path(__file__).resolve().parents[3] -def _chunked(values: Iterable[int], chunk_size: int) -> Iterable[List[int]]: - chunk: List[int] = [] +def _chunked(values, chunk_size): + """ + Yield successive `chunk_size`-sized chunks from `values`. + @param values: An iterable of values to chunk. + @param chunk_size: The size of each chunk to yield. + @return: An iterator of lists, each containing up to `chunk_size` elements from `values`. + """ + chunk = [] for value in values: chunk.append(value) if len(chunk) == chunk_size: @@ -38,8 +40,13 @@ def _chunked(values: Iterable[int], chunk_size: int) -> Iterable[List[int]]: yield chunk -def _bytes_to_u32_words(data: bytes) -> List[int]: - words: List[int] = [] +def _bytes_to_u32_words(data): + """ + Convert a bytes object into a list of 32-bit unsigned integers (words). + @param data: A bytes object to convert. + @return: A list of 32-bit unsigned integers representing the input bytes. + """ + words = [] for i in range(0, len(data), 4): block = data[i : i + 4] value = 0 @@ -49,27 +56,49 @@ def _bytes_to_u32_words(data: bytes) -> List[int]: return words -def _format_word_array(words: List[int], per_line: int = 12) -> str: - formatted_lines: List[str] = [] +def _format_word_array(words, per_line = 12): + """ + Format a list of 32-bit unsigned integers as a C-style array. + @param words: A list of 32-bit unsigned integers to format. + @param per_line: The number of elements per line in the output array. + @return: A string representing the formatted C-style array. + """ + formatted_lines = [] for chunk in _chunked(words, per_line): joined = ", ".join(f"0x{value:08x}" for value in chunk) formatted_lines.append(f" {joined},") return "\n".join(formatted_lines) -def _sanitize_macro_suffix(icon_name: str) -> str: +def _sanitize_macro_suffix(icon_name): + """ + Sanitize an icon name to be used as a C++ macro suffix. + @param icon_name: The original icon name. + @return: A sanitized string suitable for use as a macro suffix. + """ upper = icon_name.upper().replace("-", "_") upper = re.sub(r"[^A-Z0-9_]", "_", upper) upper = re.sub(r"_+", "_", upper).strip("_") return upper if upper else "UNKNOWN" -def _codepoint_to_utf8_hex_escapes(codepoint: int) -> str: +def _codepoint_to_utf8_hex_escapes(codepoint): + """ + Convert a Unicode codepoint to a string of UTF-8 hex escapes. + @param codepoint: The Unicode codepoint to convert. + @return: A string of UTF-8 hex escapes representing the codepoint. + """ encoded = chr(codepoint).encode("utf-8") return "".join(f"\\x{byte:02x}" for byte in encoded) -def _format_ratio(width: int, height: int) -> str: +def _format_ratio(width, height): + """ + Format the ratio of width to height as a C-style float literal. + @param width: The width value. + @param height: The height value. + @return: A string representing the ratio as a C-style float literal. + """ if height == 0: return "1.0f" @@ -80,7 +109,12 @@ def _format_ratio(width: int, height: int) -> str: return f"{formatted}f" -def _read_icon_size(payload: Dict[str, object]) -> Tuple[float, float]: +def _read_icon_size(payload): + """ + Read the size of an icon from its payload. + @param payload: The icon payload dictionary. + @return: A tuple containing the width and height of the icon. + """ svg_payload = payload.get("svg") if not isinstance(svg_payload, dict): return 0.0, 0.0 @@ -104,7 +138,12 @@ def _read_icon_size(payload: Dict[str, object]) -> Tuple[float, float]: return 0.0, 0.0 -def _load_icons(icons_json_path: Path) -> List[Tuple[str, int, float, float]]: +def _load_icons(icons_json_path): + """ + Load and filter icons from a Font Awesome icons.json file. + @param icons_json_path: The path to the icons.json file. + @return: A list of tuples containing icon name, codepoint, width, and height. + """ with icons_json_path.open("r", encoding="utf-8") as input_file: icon_data: Dict[str, Dict[str, object]] = json.load(input_file) @@ -123,7 +162,13 @@ def _load_icons(icons_json_path: Path) -> List[Tuple[str, int, float, float]]: return icons -def _build_font_header(ttf_bytes: bytes, ttf_path: Path) -> str: +def _build_font_header(ttf_bytes, ttf_path): + """ + Build a C++ header string containing the TTF font data as a byte array. + @param ttf_bytes: The bytes of the TTF font file. + @param ttf_path: The path to the TTF font file. + @return: A string representing the C++ header with the font data. + """ words = _bytes_to_u32_words(ttf_bytes) array_body = _format_word_array(words) @@ -146,7 +191,13 @@ def _build_font_header(ttf_bytes: bytes, ttf_path: Path) -> str: ) -def _build_icon_define_header(icons: List[Tuple[str, int, int, int]], icons_path: Path) -> str: +def _build_icon_define_header(icons, icons_path): + """ + Build a C++ header string defining Font Awesome icons with their UTF-8 representations and aspect ratios. + @param icons: A list of tuples containing icon name, codepoint, width, and height. + @param icons_path: The path to the icons.json file. + @return: A string representing the C++ header with icon definitions. + """ if not icons: raise ValueError("No solid icons were found in the input icons metadata.") @@ -180,7 +231,7 @@ def _build_icon_define_header(icons: List[Tuple[str, int, int, int]], icons_path return "\n".join(lines) -def _parse_args(argv: List[str]) -> Tuple[Path, Path, Path, Path]: +def _parse_args(argv): """ Supported forms: - fontAwesomeCreator.py @@ -199,7 +250,7 @@ def _parse_args(argv: List[str]) -> Tuple[Path, Path, Path, Path]: raise ValueError(usage) -def main(argv: List[str]) -> int: +def main(argv): try: input_ttf, icons_json, out_font_header, out_define_header = _parse_args(argv) diff --git a/src/viewer/scripts/shaderCacheCreator.py b/src/viewer/scripts/shaderCacheCreator.py index 023a333..66c1f9c 100644 --- a/src/viewer/scripts/shaderCacheCreator.py +++ b/src/viewer/scripts/shaderCacheCreator.py @@ -1,3 +1,5 @@ +# Copyright © 2025 CCP ehf. + import os import re import sys